This tutorial is the last in a series of tutorials on rollup.js. I will analyze the vue.js source code packaging process based on vue.js framework and give you an in-depth understanding of how complex front-end frameworks are packaged using rollup.js. With this tutorial, you will be able to use rollup.js for your own projects.

Pre-learning — basics

To understand the package source of vue.js, you need to know the following:

  • fsModule:Node.jsBuilt-in modules for local file system processing;
  • pathModule:Node.jsBuilt-in module for local path resolution;
  • bubleModule: forES6+Syntax compilation;
  • flowModule: forJavascriptSource code static inspection;
  • zlibModule:Node.jsBuilt-in module for usegzipAlgorithm for file compression;
  • terserModule: forJavascriptCode compression and beautification.

I’ve compiled these basics into a pre-learning tutorial called “10 Minutes for Quick Mastery of Rollup.js — Pre-learning Basics” for those interested.

Pre-learning – rollup.js plugin

The rollup.js advanced tutorial explains some of the common plug-ins in rollup.js:

  • rollup-plugin-resolve: Integrate external module code;
  • rollup-plugin-commonjsSupport:CommonJSModule;
  • rollup-plugin-babelCompile:ES6+Syntax for theES2015;
  • rollup-plugin-jsonSupport:jsonModule;
  • rollup-plugin-uglify: code compression (not supportedESModule);

In order to understand the package source of vue.js, we also need to learn the following rollup.js plugin and knowledge:

  • rollup-plugin-bublePlug-ins: compileES6+Syntax for theES2015, no configuration is requiredbabelMore light;
  • rollup-plugin-aliasPlugins: replace aliases in module paths;
  • rollup-plugin-flow-no-whitespacePlugins: RemoveflowStatic type checking code;
  • rollup-plugin-replacePlugins: replace variables in code with specified values;
  • rollup-plugin-terserPlug-ins: Code compression, replaceuglifyTo supportESThe module.
  • introandoutroConfiguration: Add code comments within a code block.

For those of you who are not familiar with these plugins, I have prepared another pre-learning tutorial: 10 Minutes to Quickly Master Rollup.js — Pre-learning The Rollup.js plugin.

Vue.js source code package

Clone the vue. js source code locally:

git clone https://github.com/vuejs/vue.git
Copy the code

Install dependencies:

cd vue
npm i
Copy the code

Open package.json to view scripts:

"scripts": {
  "build": "node scripts/build.js",
  "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
  "build:weex": "npm run build -- weex",
}
Copy the code

Let’s start by packing with the build directive:

$NPM run build > [email protected] build/Users/Sam/WebstormProjects/vue > node scripts/build. Js Dist/vue.runtime.com mon. Js 209.20 KB dist/vue.com mon. Js 288.22 KB dist/vue runtime. Esm. Js 209.18 KB dist/vue esm. Js Dist /vue.runtime.js 216.9 KB dist/vue.runtime.min.js 216.10 KB (gzipped: Dist /vue.js dist/vue.min.js 13.86 KB) Packages/VUe-template-compiler /build.js 121.88 KB Packages/Vue-template-compiler /browser.js 228.17 KB Packages/vue - server - the renderer/build js 220.73 KB packages/vue - server - the renderer/basic. Js 304.00 KB Packages/vue - server - the renderer/server - the plugin. Js 2.92 KB packages/vue - server - the renderer/client - plugin. Js 3.03 KBCopy the code

After successful packaging, the following packaging files are created in the dist directory:

build:ssr
build:weex
build:ssr

$NPM run build: SSR > [email protected] build: SSR/Users/Sam/WebstormProjects/vue > NPM run build - Web - the runtime - CJS, web server - the renderer > [email protected] build/Users/Sam/WebstormProjects/vue > node scripts/build. Js"web-runtime-cjs,web-server-renderer"Dist/vue.runtime.com mon. Js 209.20 KB packages/vue - server - the renderer/build js 220.73 KB packages/vue - server - the renderer/basic. Js 304.00 KB Packages /vue-server-renderer/server-plugin.js 2.92 KB Packages /vue-server-renderer/client-plugin.js 3.03 KBCopy the code

Try build: Weex:

$NPM run build: weex > [email protected] build: weex/Users/Sam/WebstormProjects/vue > NPM run build - weex > [email protected] build/Users/Sam/WebstormProjects/vue > node scripts/build. Js"weex"Packages/weex - vue - framework/factory. Js 193.79 KB packages/weex - vue - framework/index. Js 5.68 KB Packages/weex - the template - the compiler/build js 109.11 KBCopy the code

Scripts /build.js are executed via Node, but the parameters attached are different:

node scripts/build.js # build
node scripts/build.js "web-runtime-cjs,web-server-renderer" # build:ssr
node scripts/build.js "weex" # build:weex
Copy the code

Scripts /build.js is the key to interpreting vue.js source code package. Here we will analyze vue.js source code packaging process.

Vue. Js packaging process analysis

The vue. js source code package is based on the ROLLup. js API, which can be roughly divided into five steps, as shown below:

  • Step 1: Create the dist directory. Check whether the dist directory exists. If not, create it.
  • Step 2: GeneraterollupConfiguration file. Generated through scripts/config.jsrollupConfiguration file;
  • Step 3:rollupConfigure file filtering. Based on the parameters passed in, yesrollupFilter the content of the configuration file to exclude unnecessary packaging items.
  • Step 4: Iterate through the configuration package and generate the package source code. Through the configuration file project, passrollupAPI to package, and generate packaged source code.
  • Step 5: source code output file, gzip compression test. If the output is the final product, passterserPerform minimal compression and passzlibGzip compression test, and output test results in the console, and finally output the source content to the specified file, complete packaging.

Vue.js package source code analysis

Below we will go into vue.js packaging source code, analysis of packaging principles and details.

Friendship tip: it is recommended to read all four tutorials provided before reading the source code:

  • 10 minute Quick Start Rollup.js
  • 10 minutes quick Progress Rollup.js
  • Quick Mastery of Rollup.js in 10 Minutes: The Basics of Pre-learning
  • Rollup. js plugin for Pre-learning rollup.js in 10 Minutes

Creating the dist directory

NPM run build starts with 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')}Copy the code

The first five lines import five modules each, the purpose of which is detailed in the pre-learning tutorial. Line 7 uses a synchronization method to determine whether the dist directory exists, and if not, to create it.

Generate the rollup configuration

After generating the dist directory, the rollup configuration file is generated with the following code:

// scripts/build.js
let builds = require('./config').getAllBuilds()
Copy the code

The code is just one sentence, but it does a lot of things. It first loads the scripts/config.js module and then calls the getAllBuilds() method within it. Scripts /config.js is loaded with the following contents:

// 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')
Copy the code

The purpose and usage of these plug-ins are described in both the advanced and pre-tutorial tutorials.

const version = process.env.VERSION || require('.. /package.json').version
const weexVersion = process.env.WEEX_VERSION || require('.. /packages/weex-vue-framework/package.json').version
Copy the code

The above code gets the Vue version number and Weex version number from package.json.

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

The above code generates the banner text, which is written at the top of the file after the Vue code is packaged.

const weexFactoryPlugin = {
  intro () {
    return 'module.exports = function weexFactory (exports, document) {'
  },
  outro () {
    return '} '}}Copy the code

The above code is only used to package the weeX-Factory source code:

// 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]
}
Copy the code

Next we import the scripts/alias.js module:

const aliases = require('./alias')
Copy the code

The alias.js module outputs an object in which all aliases and their corresponding absolute paths are defined:

// 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')}Copy the code

The resolve() method is defined in this module to generate an absolute path:

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

__dirname is the path of the current module, i.e. Scripts/directory,.. / represents the upper-level directory, the root of the project, which is then combined with the relative path passed in to form the final result using the path.resolve() method. Back to the scripts/config.js module, we continue:

// scripts/config.js
const resolve = p= > {
  // Get the alias of the path
  const base = p.split('/') [0]
  // Find if the alias exists
  if (aliases[base]) { 
    // If the alias exists, merge the path of the alias with the file name
    return path.resolve(aliases[base], p.slice(base.length + 1))}else {
    // If the alias does not exist, merge the project root path with the incoming path
    return path.resolve(__dirname, '.. / ', p) 
  }
}
Copy the code

Config.js also defines a resolve() method, which receives a path parameter P, assuming p is web/ entry-Runtime.js, then the base obtained in the first step is Web, and then the alias object aliases output by the alias module is checked to see if the corresponding alias exists. The corresponding alias for the Web module exists, and its value is:

web: resolve('src/platforms/web')
Copy the code

Therefore, the actual path of the alias is merged with the file name to obtain the real path of the file. The file name can be obtained by:

p.slice(base.length + 1)
Copy the code

If the incoming path is: dist/vue.runtime.com mon. Js, will find the alias dist, which doesn’t exist, so does the other path, the project root directory path joining together with the incoming parameters, which is to execute the following code:

return path.resolve(__dirname, '.. / ', p)
Copy the code

This is similar to the implementation of the scripts/alias.js module. The following builds variable is defined in the config.js module:

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
  }
}
Copy the code

The config. Js module defines a genConfig(name) method to resolve the problem, since the config item has the name of the old rollup.js version and has been deprecated in the new version:

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
}
Copy the code

The purpose of this method is to convert the rollup.js configuration from the old version to the new version format. For the plugins part, each packaged project uses the Replace, Flow, Buble, and Alias plugins, and the rest of the custom plugins are merged into plugins using the following code:

plugins: [].concat(opts.plugins || []),
Copy the code

The genConfig() method also determines whether the environment variable NODE_ENV needs to be replaced:

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

The above code determines if the env argument is present in the opts passed in, and if so, replaces the process.env.node_env part of the code with json.stringify (opts.env) : , if the env value passed is development, the result generated is development with double quotes

"development"
Copy the code

In addition, the genConfig() method also stores the key of the Builds object in the Config object:

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

If the key for builds is Web-runtime-cjs, the resulting config would be:

config = {
  '_name': 'web-runtime-cjs'
}
Copy the code

Finally, the config.js module defines the getAllBuilds() method:

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = (a)= > Object.keys(builds).map(genConfig)
}
Copy the code

The method first determines whether the environment variable TARGET is defined. The TARGET environment variable is not defined in the three build methods, so the logic in else will be executed and a getBuild() and getAllBuilds() method will be exposed. The getAllBuilds() method retrieves the key array of builds objects, iterates over and calls the genConfig() method to generate the configuration objects so that the rollup configuration is generated.

Rollup Configures filtering

Back to the scripts/build.js module, after the configuration is generated, the configuration items will be filtered, because each packaging mode will output different results, the source of the filtering section is as follows:

// 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})}Copy the code

First, analyze the build command, which is actually executed as follows:

node scripts/build.js
Copy the code

So the contents of process.argv are:

[ '/ Users/Sam /. NVM/versions/node/v11.2.0 / bin/node'.'/Users/sam/WebstormProjects/vue/scripts/build.js' ]
Copy the code

Process. argv[2] is not present, so else is executed:

builds = builds.filter(b= > {
  return b.output.file.indexOf('weex') = = =- 1
})
Copy the code

This code is used to exclude weex code packaging, by whether output.file contains WEEx string to determine whether weeX code. The build: SSR command is as follows:

node scripts/build.js "web-runtime-cjs,web-server-renderer"
Copy the code

The value of process.argv is:

[ '/ Users/Sam /. NVM/versions/node/v11.2.0 / bin/node'.'/Users/sam/WebstormProjects/vue/scripts/build.js'.'web-runtime-cjs,web-server-renderer' ]
Copy the code

Process. argv[2] has a value of Web-runtime-cjs and web-server-renderer, so the logic in if is executed:

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)
Copy the code

This method first separates the parameters into an array of filters with commas and then iterates through the builds array looking for any configuration item in output.file or _NAME that contains any of the filters. For example, if the first element of the filters is web-runtime-cjs, it will look for the configuration item that contains the web-runtime-cjs in output.file or _name. The _name will point to the key of the configuration item.

'web-runtime-cjs': {
  entry: resolve('web/entry-runtime.js'),
  dest: resolve('dist/vue.runtime.common.js'),
  format: 'cjs',
  banner
}
Copy the code

The configuration is then retained and eventually packaged.

A rollup packaging

After configuration filtering is complete, the packaging function is called:

build(builds)
Copy the code

The build function is defined as follows:

function build (builds) {
  let built = 0 // Current package item number
  const total = builds.length // The total number of times you need to pack
  const next = (a)= > {
    buildEntry(builds[built]).then((a)= > {
      built++ // Add 1 after packing
      if (built < total) {
        next() // If the package number is less than the total number of packages, continue next()
      }
    }).catch(logError) // Output error information
  }

  next() // Call next()
}
Copy the code

The build() function takes builds, iterate over them, and calls buildEntry() to perform the actual packing logic. BuildEntry () returns a Promise object and calls logError(e) to print an error message if there is an error:

function logError (e) {
  console.log(e)
}
Copy the code

The packaged core function is buildEntry(config)

function buildEntry (config) {
  const output = config.output // Get the output of config
  const { file, banner } = output // Get file and banner in output
  const isProd = /min\.js$/.test(file) // Check whether file ends in min.js, if so, flag isProd as true
  return rollup.rollup(config) // Perform rollup packaging
    .then(bundle= > bundle.generate(output)) // Generate the source code for the packaged result
    .then(({ code }) = > { // Get the source code generated by the package
      if (isProd) { // Check whether it is isProd
        const minified = (banner ? banner + '\n' : ' ') + terser.minify(code, { // Perform code minimization packaging and manually add the banner in the code title, as minimization of packaging causes comments to be removed
          output: { 
            ascii_only: true // Only ASCII characters are supported
          },
          compress: {
            pure_funcs: ['makeMap'] // Filter the makeMap function
          }
        }).code // Get the code to minimize packaging
        return write(file, minified, true) // Write code to the output path
      } else {
        return write(file, code) // Write code to the output path}})}Copy the code

If you understand how rollup works and how terser is used, the above code is not difficult to understand. The difference is that we add banner annotations manually and output code files manually instead of using rollup. The previous approach we adopted was:

const bundle = await rollup.rollup(input) // Get the bundle object
bundle.write(output) // Output the packaged object to a file
Copy the code

The method adopted by vue.js is as follows:

const bundle = await rollup.rollup(input) // Get the bundle object
const { code, map } = await bundle.generate(output) // Generate the source code and source map from the bundle
Copy the code

Get the source code from the bundle and manually export it to a file.

Source output

The source code output is primarily a call to the write() function, which requires three arguments:

  • Dest: the absolute path to the output file, which can be obtained from output.file.
  • Code: indicates the source codebundle.generate()To obtain;
  • Zip: whether a gzip compression test is required. If isProd is true, zip is true; otherwise, zip is false.
function write (dest, code, zip) {
  return new Promise((resolve, reject) = > {
    function report (extra) { // Outputs the log function
      console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ' ')) // Prints the file name, file capacity, and gzip compression test results
      resolve()
    }

    fs.writeFile(dest, code, err => {
      if (err) return reject(err) Reject () is called if an error is reported
      if (zip) { // If isProd then gzip test
        zlib.gzip(code, (err, zipped) => { // Use gzip to compress source code
          if (err) return reject(err)
          report(' (gzipped: ' + getSize(zipped) + ') ') // Get the gzip string length and giZp capacity after the test is successful})}else {
        report() // Output logs}})})}Copy the code

There are a few details to note here. The first is to get the relative path of the current command line path to the final build file:

path.relative(process.cwd(), dest)
Copy the code

The second is to call the blue() function to generate blue text on the command line:

function blue (str) {
  return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
}
Copy the code

The third is the method of obtaining file capacity:

function getSize (code) {
  return (code.length / 1024).toFixed(2) + 'kb'
}
Copy the code

These three methods are not difficult to understand, but they are very practical, you can learn a lot in the development process.

conclusion

We can find that when we have the basic knowledge, and then analyze vue.js source code packaging process is not complex, so it is suggested that we can draw lessons from this way of learning in the work, the basic knowledge point out first, figure out after the complex source code. Rollup. Js10 minutes tutorial is over. If you have any suggestions for this tutorial, please leave a comment.