Vue.js source code construction

Key words: look at the build.js file definition, filter and build depending on the configuration

Vue.js source code construction

Vue builds with Rollup

Rollup Like webpack, it is a construction tool. Webpack is more powerful, rollup is more suitable for the compilation of JavaScript libraries, and is lighter and more code friendly. Therefore, Vue.js chose rollup for construction, and its construction related configurations are in the scripts directory.

Build script

vue is published on NPM. Each NPM package (equivalent to a project) needs a package.json file to describe it. Its content is actually a standard JSON object. We usually configure the script field as the execution script of NPM.

{
  "name": "vue",
  "version": "2.6.13",
  "main": "dist/vue.runtime.common.js",
  "module": "dist/vue.runtime.esm.js",
  "scripts": {
    // These three tasks are related to construction. "Build" is Vue.js for establishing web platform, "build ssr" is output, which is related to server renderer, "build weex" is related to weex. The function is to build Vue.js. The last two commands add some environment parameters based on the first command.
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
  }
}
fieldmeaning
nameThe name is unique
versionedition
mainnpm package entry. When importing "Vue", the entry will be found through main
moduleModule is very similar to main. Above webpack2, module is used as the default entry. It can also be said that the default entry of Vue.js is the esm.js file
scriptsnpm provides npm scripts, which define many scripts. Each script is a task. Different scripts can execute different tasks through npm run value (dev, bulid).

By default, many versions of Vue.js have been built under the dist file.

Construction process

Why can so many versions of Vue.js be built? Then we must first understand its construction process. For example, when npm run build is executed, the node scripts/build.js script is actually executed, that is, the JS of the build.js file in the scripts folder is run.

(1) Define dependent modules

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

(2) Get all the configurations required for the build from the config.js configuration file

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

The bottom of the config.js file exposes a method exports.getAllBuilds(), which is a function. Get an array through Object.keys(builds), and then call the genConfig() function through the map() method

// scripts/config.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)

1. The Object.keys() method returns an array of enumerable properties of a given object

2. Enumeration refers to enumerating columns one by one

3. Three parameters of map: array element, element index, original array itself, ar.map ((currentvalue, index, array) = > {})

builds

In fact, builds is an object, and each key corresponds to an object, which is actually the configuration compiled by different versions of Vue.js

Entry: entry. Pass the string web/entry-runtime.js through the resolve() function
dest: target
Format: file format. Different versions can be built through different formats
banner: local variable, defining annotation

entry and dest

So finally, take entry-runtime.js as the compilation entry, and finally generate the vue.runtime.common.dev.js file

// scripts/config.js
const builds = {
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  }
}

The resolve() function receives a parameter, takes the first value in front of the base / web in the entry, and p.slice(base.length + 1) returns entry-runtime.js

In dest, the base value is dist, but there is no dist in alias.js, so directly return to the large directory ('...') of the current directory (_dirname) and find the vue.runtime.common.dev.js file

// scripts/config.js
const aliases = require('./alias')
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)
  }
}

The aliases() function exports an object at the end of the alias.js file. There are many key s in the object. The resolve() function returns a string, which is a directory. Resolve() calls path.resolve(), which is a path resolution method provided by node.js__ dirname refers to the current directory. Go up to the next level, find the large directory, and then pass the parameter p

Therefore, the alias.js file provides a mapping relationship to the final real file address

// scripts/alias.js
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'),
  sfc: resolve('src/sfc')
}
format

The format attribute indicates the format of the build, cjs indicates that the built file follows the CommonJS specification, and es indicates that the built file follows the ES Module specification. UMD means that the built file follows the UMD specification.

// dist/vue.runtime.esm.js
export default Vue;
// dist/vue.runtime.common.js
module.exports = require('./vue.runtime.common.dev.js')
// dist/vue.js
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.Vue = factory());
}(this, function () { 'use strict';

banner

You can know the version, creator and License

// scripts/cofig.js
const banner =
  '/*!\n' +
 `* Vue.js v${version}\n`+
 `* (c) 2014-${new Date().getFullYear()} Evan You\n`+
  ' * Released under the MIT License.\n' +
  ' */'
// dist/vue.min.js
/*!
 * Vue.js v2.6.14
 * (c) 2014-2021 Evan You
 * Released under the MIT License.
 */

genConfig()

Back to scripts/cofig.js, the array calls the genConfig() function:
opts: genConfig gets the key. opts is the object corresponding to the key in builds
Config: construct a new config object. The data structure of this object is the configuration structure corresponding to the real Rollup. The entry is only the entry defined by ourselves, but it is called input in Rollup. So config is the final configuration for Rollup.

In general, we map and transform builds to generate the final configuration required by Rollup. It is also an array. The final generated array is returned to the build.js file.

// scripts/cofig.js
function genConfig (name) {
  const opts = builds[name]
  const config = {
    input: opts.entry,
    external: opts.external,
    plugins: [
      flow(),
      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)
      }
    }
  }
}

(3) Filter the configuration and filter out what we don't need, leaving only what we need to compile, so that we can build Vue.js for different purposes

process.argv[2] corresponds to -- weex and -- web in the package.json file. If there are these parameters, the ones that do not need to be packaged will be filtered out through the filter. If there are no parameters, the weex will be filtered out.

// scripts/build.js
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
  })
}

(4) Call the build() function to do the real build process

The build() function defines a next() method. When the next() method is executed, it calls buildeentry () and the counter build.

// scripts/build.js
build(builds)

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

Buildeentry () gets the config of builds. Config is the config required for Rollup's final compilation. After config compilation, it gets the bundle. The bundle generates output through generate(), which corresponds to the generated target. The code may be modified. For example, judge whether to compress JS. isProd defines that the file ends in min.js, then compress terser.minify again, and finally call the write() method to generate it in the dist directory.

// scripts/build.js
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)
      }
    })
}

Personal understanding: the execution code and definition code are generally written on the js file to facilitate viewing the execution process, so function is generally written on the back of the js file.

Tags: Vue.js

Posted on Sun, 28 Nov 2021 00:20:29 -0500 by prashanth0626