10分钟快速精通rollup.js——Vue.js源码打包原理深度分析

栏目: JavaScript · 发布时间: 6年前

内容简介:本教程是rollup.js系列教程的最后一篇,我将基于目前非常流行的Vue.js框架,深度分析Vue.js源码打包过程,让大家深入理解复杂的前端框架是如何利用rollup.js进行打包的。通过这一篇教程的学习,相信大家可以更好地应用rollup.js为自己的项目服务。说明:本教程基于Vue.js 2.5.17-beta.0版本源码要看懂Vue.js的打包源码,需要掌握以下知识点:

本教程是rollup.js系列教程的最后一篇,我将基于目前非常流行的Vue.js框架,深度分析Vue.js源码打包过程,让大家深入理解复杂的前端框架是如何利用rollup.js进行打包的。通过这一篇教程的学习,相信大家可以更好地应用rollup.js为自己的项目服务。

说明:本教程基于Vue.js 2.5.17-beta.0版本源码

前置学习——基础知识

要看懂Vue.js的打包源码,需要掌握以下知识点:

  • fs模块:Node.js内置模块,用于本地文件系统处理;
  • path模块:Node.js内置模块,用于本地路径解析;
  • buble模块:用于ES6+语法编译;
  • flow模块:用于Javascript源码静态检查;
  • zlib模块:Node.js内置模块,用于使用gzip算法进行文件压缩;
  • terser模块:用于Javascript代码压缩和美化。

我将这些基础知识点整理成一篇前置学习教程: 《10分钟快速精通rollup.js——前置学习之基础知识篇》 ,感兴趣的小伙伴可以看看。

前置学习——rollup.js插件

rollup.js进阶教程中讲解了rollup.js的部分常用插件:

  • rollup-plugin-resolve:集成外部模块代码;
  • rollup-plugin-commonjs:支持CommonJS模块;
  • rollup-plugin-babel:编译ES6+语法为ES2015;
  • rollup-plugin-json:支持json模块;
  • rollup-plugin-uglify:代码压缩(不支持ES模块);

为了理解Vue.js的打包源码,我们还需要学习以下rollup.js插件及知识:

  • rollup-plugin-buble插件:编译ES6+语法为ES2015,无需配置,比babel更轻量;
  • rollup-plugin-alias插件:替换模块路径中的别名;
  • rollup-plugin-flow-no-whitespace插件:去除flow静态类型检查代码;
  • rollup-plugin-replace插件:替换代码中的变量为指定值;
  • rollup-plugin-terser插件:代码压缩,取代uglify,支持ES模块。
  • intro和outro配置:在代码块内添加代码注释。

我为还不熟悉这些插件的小伙伴准备了另一篇前置学习教程: 《10分钟快速精通rollup.js——前置学习之rollup.js插件篇》

Vue.js源码打包

Vue.js的打包过程并不复杂,首先要将Vue.js源码clone到本地:

git clone https://github.com/vuejs/vue.git
复制代码

安装依赖:

cd vue
npm i
复制代码

打开package.json查看scripts:

"scripts": {
  "build": "node scripts/build.js",
  "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
  "build:weex": "npm run build -- weex",
}
复制代码

我们先通过build指令进行打包:

$ npm run build

> vue@2.5.17-beta.0 build /Users/sam/WebstormProjects/vue
> node scripts/build.js

dist/vue.runtime.common.js 209.20kb
dist/vue.common.js 288.22kb
dist/vue.runtime.esm.js 209.18kb
dist/vue.esm.js 288.20kb
dist/vue.runtime.js 219.55kb
dist/vue.runtime.min.js 60.24kb (gzipped: 21.62kb)
dist/vue.js 302.27kb
dist/vue.min.js 85.19kb (gzipped: 30.86kb)
packages/vue-template-compiler/build.js 121.88kb
packages/vue-template-compiler/browser.js 228.17kb
packages/vue-server-renderer/build.js 220.73kb
packages/vue-server-renderer/basic.js 304.00kb
packages/vue-server-renderer/server-plugin.js 2.92kb
packages/vue-server-renderer/client-plugin.js 3.03kb
复制代码

打包成功后会在dist目录下创建下列打包文件:

10分钟快速精通rollup.js——Vue.js源码打包原理深度分析

以上就是使用build指令对Vue.js源码进行打包的过程,除此之外,Vue.js还提供了另外两种打包方式:"build:ssr"和"build:weex",先尝试"build:ssr"指令:

$ npm run build:ssr

> vue@2.5.17-beta.0 build:ssr /Users/sam/WebstormProjects/vue
> npm run build -- web-runtime-cjs,web-server-renderer


> vue@2.5.17-beta.0 build /Users/sam/WebstormProjects/vue
> node scripts/build.js "web-runtime-cjs,web-server-renderer"

dist/vue.runtime.common.js 209.20kb
packages/vue-server-renderer/build.js 220.73kb
packages/vue-server-renderer/basic.js 304.00kb
packages/vue-server-renderer/server-plugin.js 2.92kb
packages/vue-server-renderer/client-plugin.js 3.03kb
复制代码

再尝试"build:weex":

$ npm run build:weex

> vue@2.5.17-beta.0 build:weex /Users/sam/WebstormProjects/vue
> npm run build -- weex


> vue@2.5.17-beta.0 build /Users/sam/WebstormProjects/vue
> node scripts/build.js "weex"

packages/weex-vue-framework/factory.js 193.79kb
packages/weex-vue-framework/index.js 5.68kb
packages/weex-template-compiler/build.js 109.11kb
复制代码

通过命令行日志可以看出这两个指令和build指令没有本质区别,都是通过node执行scripts/build.js源码,只是附带的参数不同:

node scripts/build.js # build
node scripts/build.js "web-runtime-cjs,web-server-renderer" # build:ssr
node scripts/build.js "weex" # build:weex
复制代码

可见scripts/build.js是解读Vue.js源码打包的关键。下面我们就来分析Vue.js的源码打包流程。

Vue.js打包流程分析

Vue.js源码打包基于rollup.js的API,流程大致可分为五步,如下图所示:

10分钟快速精通rollup.js——Vue.js源码打包原理深度分析
  • 第一步:创建dist目录。检查是否存在dist目录,如果不存在,则进行创建;
  • 第二步:生成rollup配置文件。通过scripts/config.js生成rollup的配置文件;
  • 第三步:rollup配置文件过滤。根据传入的参数,对rollup配置文件的内容进行过滤,排除不必要的打包项目。
  • 第四步:遍历配置打包,生成打包源码。遍历配置文件项目,通过rollup.js的API进行打包,并生成打包后的源码。
  • 第五步:源码输出文件,gzip压缩测试。如果输出的是最终产品,则通过terser进行最小化压缩并通过zlib进行gzip压缩测试,并在控制台输出测试结果,最后将源码内容输出到指定文件中,完成打包。

Vue.js打包源码分析

下面我们将深入Vue.js打包源码,解析Vue.js打包的原理和细节。

友情提示:建议阅读源码之前先将之前提供的四份教程全部看完: 《10分钟快速入门rollup.js》 《10分钟快速进阶rollup.js》 《10分钟快速精通rollup.js——前置学习之基础知识篇》 《10分钟快速精通rollup.js——前置学习之rollup.js插件篇》

创建dist目录

执行 npm run build 时,会从scripts/build.js开始执行:

// scripts/build.js
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const rollup = require('rollup')
const terser = require('terser')

if (!fs.existsSync('dist')) {
  fs.mkdirSync('dist')
}
复制代码

前5行分别导入了5个模块,这5个模块的用途在前置学习教程中已经详细过。第7行通过同步方法判断dist目录是否存在,如果不存在则通过同步方法创建dist目录。

生成rollup配置

生成dist目录后,通过以下代码生成了rollup的配置文件:

// scripts/build.js
let builds = require('./config').getAllBuilds()
复制代码

代码虽然只有短短一句,但是做了很多事情。首先它加载了scripts/config.js模块,然后调用其中的getAllBuilds()方法。下面我们来分析scripts/config.js的加载过程,加载config.js时先执行了以下内容:

// scripts/config.js
const path = require('path')
const buble = require('rollup-plugin-buble')
const alias = require('rollup-plugin-alias')
const cjs = require('rollup-plugin-commonjs')
const replace = require('rollup-plugin-replace')
const node = require('rollup-plugin-node-resolve')
const flow = require('rollup-plugin-flow-no-whitespace')
复制代码

这些插件的用途和用法在进阶教程和前置教程中都有介绍。

const version = process.env.VERSION || require('../package.json').version
const weexVersion = process.env.WEEX_VERSION || require('../packages/weex-vue-framework/package.json').version
复制代码

上述代码是从package.json中获取Vue的版本号和Weex的版本号。

const banner =
  '/*!\n' +
  ` * Vue.js v${version}\n` +
  ` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
  ' * Released under the MIT License.\n' +
  ' */'
复制代码

上述代码生成了banner文本,在Vue代码打包后,会写在文件顶部。

const weexFactoryPlugin = {
  intro () {
    return 'module.exports = function weexFactory (exports, document) {'
  },
  outro () {
    return '}'
  }
}
复制代码

上述代码仅用于打包weex-factory源码时使用:

// Weex runtime factory
'weex-factory': {
  weex: true,
  entry: resolve('weex/entry-runtime-factory.js'),
  dest: resolve('packages/weex-vue-framework/factory.js'),
  format: 'cjs',
  plugins: [weexFactoryPlugin]
}
复制代码

接下来导入了scripts/alias.js模块:

const aliases = require('./alias')
复制代码

alias.js模块输出了一个对象,这个对象中定义了所有的别名及其对应的绝对路径:

// scripts/alias.js
const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  entries: resolve('src/entries'),
  sfc: resolve('src/sfc')
}
复制代码

这个模块中定义了resolve()方法,用于生成绝对路径:

const resolve = p => path.resolve(__dirname, '../', p)
复制代码

__dirname为当前模块对应的路径,即scripts/目录,../表示上一级目录,即项目的根目录,然后通过path.resolve()方法将项目的根目录与传入的相对路径结合起来形成最终结果。回到scripts/config.js模块,我们继续向下执行:

// scripts/config.js
const resolve = p => {
  // 获取路径的别名
  const base = p.split('/')[0]
  // 查找别名是否存在
  if (aliases[base]) { 
    // 如果别名存在,则将别名对应的路径与文件名进行合并
    return path.resolve(aliases[base], p.slice(base.length + 1)) 
  } else {
    // 如果别名不存在,则将项目根路径与传入路径进行合并
    return path.resolve(__dirname, '../', p) 
  }
}
复制代码

config.js也定义了一个resolve方法,该方法接收一个路径参数p,假设p为web/entry-runtime.js,则第一步获取的base为web,然后到alias模块输出的对象aliases中寻找对应的别名是否存在,web模块对应的别名是存在的,它的值为:

web: resolve('src/platforms/web')
复制代码

所以会将别名的实际路径与文件名进行拼接,获取文件的真实路径。文件名的获取方法是:

p.slice(base.length + 1)
复制代码

如果传入的路径为:dist/vue.runtime.common.js,则会查找别名dist,该别名是不存在的,所以会执行另外一条路径,将项目根路径与传入的参数路径进行拼接,即执行下面这段代码:

return path.resolve(__dirname, '../', p)
复制代码

这与scripts/alias.js模块的实现是类似的。接下来config.js模块中定义了builds变量,代码节选如下:

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.js'),
    format: 'cjs',
    banner
  }
}
复制代码

这个变量中调用resolve()方法生成了文件的真实路径,由于配置项采用的是rollup.js老版本的配置名称,在新版本中已经被废弃,所以紧接着config.js模块又定义了一个genConfig(name)方法来解决这个问题:

function genConfig (name) {
  const opts = builds[name]
  const config = {
    input: opts.entry,
    external: opts.external,
    plugins: [
      replace({
        __WEEX__: !!opts.weex,
        __WEEX_VERSION__: weexVersion,
        __VERSION__: version
      }),
      flow(),
      buble(),
      alias(Object.assign({}, aliases, opts.alias))
    ].concat(opts.plugins || []),
    output: {
      file: opts.dest,
      format: opts.format,
      banner: opts.banner,
      name: opts.moduleName || 'Vue'
    },
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }

  if (opts.env) {
    config.plugins.push(replace({
      'process.env.NODE_ENV': JSON.stringify(opts.env)
    }))
  }

  Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })

  return config
}
复制代码

这个方法的用途是将老版本的rollup.js配置转为新版本的格式。对于插件部分,每一个打包项目都会采用replace、flow、buble和alias插件,其余自定义的插件会合并到plugins中,通过以下代码实现:

plugins: [].concat(opts.plugins || []),
复制代码

genConfig()方法还判断了环境变量NODE_ENV是否需要被替换:

if (opts.env) {
    config.plugins.push(replace({
      'process.env.NODE_ENV': JSON.stringify(opts.env)
    }))
  }
复制代码

上述代码判断了传入的opts中是否存在env参数,如果存在,则会将代码中的 process.env.NODE_ENV 部分替换为 JSON.stringify(opts.env) : ,如传入的env值为development,则生成的结果为带双引号的development

"development"
复制代码

除此之外,genConfig()方法还将builds对象的key保存在config对象中:

Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })
复制代码

如果builds的key为web-runtime-cjs,则生成的config为:

config = {
  '_name': 'web-runtime-cjs'
}
复制代码

最后config.js模块定义了getAllBuilds()方法:

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
复制代码

该方法首先判断环境变量TARGET是否定义,在build的三种方法中没有定义TARGET环境变量,所以会执行else中的逻辑,else逻辑中会暴露一个getBuild()方法和getAllBuilds()方法,getAllBuilds()方法会获取builds对象的key数组,进行遍历并调用genConfig方法生成配置对象,这样rollup的配置就生成了。

rollup配置过滤

我们回到scripts/build.js模块,配置生成完毕后,将对配置项进行过滤,因为每一种打包模式都将输出不同的结果,过滤部分的源码如下:

// scripts/build.js
// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}
复制代码

首先分析build命令,该命令实际执行指令为:

node scripts/build.js
复制代码

所以process.argv的内容为:

[ '/Users/sam/.nvm/versions/node/v11.2.0/bin/node',
  '/Users/sam/WebstormProjects/vue/scripts/build.js' ]
复制代码

不存在process.argv[2],所以会执行else中的内容:

builds = builds.filter(b => {
  return b.output.file.indexOf('weex') === -1
})
复制代码

这段代码的用途是排除weex的代码打包,通过output.file是否包含weex字符串判断是否为weex代码。build:ssr命令实际执行指令为:

node scripts/build.js "web-runtime-cjs,web-server-renderer"
复制代码

此时process.argv的值为:

[ '/Users/sam/.nvm/versions/node/v11.2.0/bin/node',
  '/Users/sam/WebstormProjects/vue/scripts/build.js',
  'web-runtime-cjs,web-server-renderer' ]
复制代码

process.argv[2]的值为web-runtime-cjs,web-server-renderer,所以会执行if中的逻辑:

const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
复制代码

这个方法首先将参数通过逗号分隔为一个filters数组,然后遍历builds数组,寻找output.file或_name中任一个包含filters中任一个的配置项。比如filters的第一个元素为:web-runtime-cjs,则会寻找output.file或_name中包含web-runtime-cjs的配置项,_name之前分析过,它指向配置项的key,此时会找到下面的配置项符合条件:

'web-runtime-cjs': {
  entry: resolve('web/entry-runtime.js'),
  dest: resolve('dist/vue.runtime.common.js'),
  format: 'cjs',
  banner
}
复制代码

那么该配置就会被保留,并最终被打包。

rollup打包

配置过滤完之后就会调用打包函数:

build(builds)
复制代码

build函数定义如下:

function build (builds) {
  let built = 0 // 当前打包项序号
  const total = builds.length // 需要打包的总次数
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++ // 打包完成后序号加1
      if (built < total) {
        next() // 如果打包序号小于打包总次数,则继续执行next()函数
      }
    }).catch(logError) // 输出错误信息
  }

  next() // 调用next()函数
}
复制代码

build()函数接收builds参数,进行遍历,并调用buildEntry()函数执行实际的打包逻辑,buildEntry()函数返回一个Promise对象,如果出错,会调用logError(e)函数打印报错信息:

function logError (e) {
  console.log(e)
}
复制代码

打包的核心函数是buildEntry(config)

function buildEntry (config) {
  const output = config.output // 获取config的output配置项
  const { file, banner } = output // 获取output中的file和banner
  const isProd = /min\.js$/.test(file) // 判断file中是否以min.js结尾,如果是则标记isProd为true
  return rollup.rollup(config) // 执行rollup打包
    .then(bundle => bundle.generate(output)) // 将打包的结果生成源码
    .then(({ code }) => { // 获取打包生成的源码
      if (isProd) { // 判断是否为isProd
        const minified = (banner ? banner + '\n' : '') + terser.minify(code, { // 执行代码最小化打包,并在代码标题处手动添加banner,因为最小化打包会导致注释被删除
          output: { 
            ascii_only: true // 只支持ascii字符
          },
          compress: {
            pure_funcs: ['makeMap'] // 过滤makeMap函数
          }
        }).code // 获取最小化打包的代码
        return write(file, minified, true) // 将代码写入输出路径
      } else {
        return write(file, code) // 将代码写入输出路径
      }
    })
}
复制代码

如果理解了rollup的原理及terser的使用方法,理解上述代码并不难,这里与我们之前使用rollup.js打包不同之处在于采用了手动添加banner注释和手动输出代码文件,而之前都是rollup.js自动输出。之前我们采用的方法为:

const bundle = await rollup.rollup(input) // 获取打包对象bundle
bundle.write(output) // 将打包对象输出到文件
复制代码

而Vue.js采用的方法是:

const bundle = await rollup.rollup(input) // 获取打包对象bundle
const { code, map } = await bundle.generate(output) // 根据bundle生成源码和source map
复制代码

通过bundle获取源码,然后手动输出到文件中。

源码输出

源码输出主要是调用write()函数,这里需要提供3个参数:

  • dest:输出文件的绝对路径,通过output.file获取;
  • code:源码字符串,通过bundle.generate()获取;
  • zip:是否需要进行gzip压缩测试,如果isProd为true,则zip为true,反之为false。
function write (dest, code, zip) {
  return new Promise((resolve, reject) => {
    function report (extra) { // 输出日志函数
      console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || '')) // 打印文件名称、文件容量和gzip压缩测试结果
      resolve()
    }

    fs.writeFile(dest, code, err => {
      if (err) return reject(err) // 如果报错则直接调用reject()方法
      if (zip) { // 如果isProd则进行gzip测试
        zlib.gzip(code, (err, zipped) => { // 通过gzip对源码进行压缩测试
          if (err) return reject(err)
          report(' (gzipped: ' + getSize(zipped) + ')') // 测试成功后获取gzip字符串长度并输出gizp容量
        })
      } else {
        report() // 输出日志
      }
    })
  })
}
复制代码

这里有几个细节需要注意,第一是获取当前命令行路径到最终生成文件的相对路径:

path.relative(process.cwd(), dest)
复制代码

第二是调用blue()函数生成命令行蓝色的文本:

function blue (str) {
  return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
}
复制代码

第三是获取文件容量的方法:

function getSize (code) {
  return (code.length / 1024).toFixed(2) + 'kb'
}
复制代码

这三个方法不难理解,但是都非常实用,大家在开发过程中可以多多借鉴。

总结

大家可以发现当我们具备了基础知识后,再分析Vue.js的源码打包过程并不复杂,所以建议大家工作中可以借鉴这种学习方式,将基础知识点先抽离出来,单独搞明白后再攻克复杂的源码。

结语

rollup.js 10分钟系列教程到此完结,对本教程有任何建议非常欢迎大家给我留言,后续大家想看哪些技术的教程也可以给我留言,教程内容较多,谢谢大家耐心看完,祝大家学习进步。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

水平营销

水平营销

[美] 菲利普·科特勒、费尔南多・德・巴斯 / 陈燕茹 / 中信出版社 / 2005-1 / 25.00元

《水平营销》阐明了相对纵向营销而言的的水平营销的框架和理论。引入横向思维来作为发现新的营销创意的又一平台,旨在获得消费者不可能向营销研究人员要求或建议的点子。而这些点子将帮助企业在产品愈加同质和超竞争的市场中立于不败之地。 《水平营销》提到: 是什么创新过程导致加油站里开起了超市? 是什么创新过程导致取代外卖比萨服务的冷冻比萨的亮相? 是什么创新过程导致巧克力糖里冒出了玩具......一起来看看 《水平营销》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具