vue原始碼解析之npm run build發生了什麼?

原鑫發表於2018-09-09

npm run build發生了什麼?

最近總是感覺對vue的一些用法和語句還是不理解,於是決定擼一下原始碼,用於加深自己對vue的理解,同時vue主要是通過rollup進行打包編譯,因為它相比webpack更加輕量,行了,廢話不多說了,開始了!

vue原始碼解析之npm run build發生了什麼?

如上圖所示,當我們執行npm runbuild命令的時候,首先package.json會將其解析為node build/build.js,執行這個目錄,我們看看這個目錄是什麼!

程式碼如果理解起來比較吃力,在檔案程式碼下面會有梳理!

直接進入到build /build.js貼程式碼!

const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const rollup = require('rollup')
const uglify = require('uglify-js')

if (!fs.existsSync('dist')) {  // 判斷是否存在dist資料夾,如果沒有則建立一個
  fs.mkdirSync('dist')  // 這也是為什麼,當我們執行完build命令後,會出現一個dist資料夾
}

let builds = require('./config').getAllBuilds()  // 引入./config中的檔案,然後執行這個檔案下的getAllBuilds()方法
//process.argv獲得附加的命令列引數 如: node app 127.0.0.1 7001 我們將會得到127.0.0.1
if (process.argv[2]) {  //  主要針對build:ssr和week形式的
  const filters = process.argv[2].split(',')  //  通過逗號分隔成陣列
  builds = builds.filter(b => {                                                         //  過濾所以.output.file和._name包含filters內容的
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1) // 檢測是否又符合條件的,有則返回true沒有則是false
  })
} else {  // 說明這塊執行的就是單純的 npm run build
  // filter out weex builds by default  //但也對weex的進行過濾
  builds = builds.filter(b => {    // 過濾輸出檔案中不包含weex的
    return b.output.file.indexOf('weex') === -1
  })
}   //  綜上所述,主要是對builds中的值進行過濾操作


build(builds)

function build (builds) {    //  對拿到的builds進行一個簡單的遍歷
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {  //  builds陣列從0到最後一個元素執行buildEntry方法
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }

  next()
}

function buildEntry (config) {  // 真正開始通過rollup對其進行編譯
  const output = config.output
  const { file, banner } = output
  const isProd = /min\.js$/.test(file)  // 匹配min.js結尾的檔案
  return rollup.rollup(config)
    .then(bundle => bundle.generate(output))
    .then(({ code }) => {
      if (isProd) {
        var minified = (banner ? banner + '\n' : '') + uglify.minify(code, {  // 判斷生成的js是否需要壓縮
          output: {
            ascii_only: true
          },
          compress: {
            pure_funcs: ['makeMap']
          }
        }).code
        return write(file, minified, true)
      } else {
        return write(file, code)
      }
    })
}

function write (dest, code, zip) {  
  return new Promise((resolve, reject) => {
    function report (extra) {  //  必要的時候,在檔案中加入console.log
      console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
      resolve()
    }

    fs.writeFile(dest, code, err => {  // 寫檔案操作
      if (err) return reject(err)
      if (zip) {
        zlib.gzip(code, (err, zipped) => {
          if (err) return reject(err)
          report(' (gzipped: ' + getSize(zipped) + ')')
        })
      } else {
        report()
      }
    })
  })
}

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

function logError (e) {
  console.log(e)
}

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


複製程式碼

下面開始對程式碼進行解析

前兩行主要是引入了一些模組,以及對dist檔案的判斷,相信通過註釋,大家一定能看得懂。

let builds = require('./config').getAllBuilds()

下面我們看一下這個builds到底是什麼,首先我們先看一下config資料夾下的程式碼

JavaScript 示例:

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
 //  對版本號的一個註釋
const banner =
  '/*!\n' +
  ' * Vue.js v' + version + '\n' +
  ' * (c) 2014-' + new Date().getFullYear() + ' Evan You\n' +
  ' * Released under the MIT License.\n' +
  ' */'
 
const weexFactoryPlugin = {
  intro () {
    return 'module.exports = function weexFactory (exports, document) {'
  },
  outro () {
    return '}'
  }
}

const aliases = require('./alias')   // alias是對檔案真實路徑的一個對映
const resolve = p => {
  const base = p.split('/')[0]   // 獲取第一個/前的名字
  if (aliases[base]) {  //  判斷aliases中是否有這個名字,同時獲取它的對映路徑
    return path.resolve(aliases[base], p.slice(base.length + 1))   //  p.slice(base.length + 1) 為 ‘/’ 後的名字
  } else {
    return path.resolve(__dirname, '../', p)  //  即dist目錄下
  }
} 
 // entry為入口,對應rollup的input  dest為出口,對應rollup的output  format為格式  banner上面有過解釋,為版本資訊註釋
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
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.js'),
    format: 'cjs',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only (ES Modules). Used by bundlers that support ES Modules,
  // e.g. Rollup & Webpack 2
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler CommonJS build (ES Modules)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Web compiler (CommonJS).
  'web-compiler': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
  },
  // Web compiler (UMD for in-browser use).
  'web-compiler-browser': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/browser.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'VueTemplateCompiler',
    plugins: [node(), cjs()]
  },
  // Web server renderer (CommonJS).
  'web-server-renderer': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-basic-renderer': {
    entry: resolve('web/entry-server-basic-renderer.js'),
    dest: resolve('packages/vue-server-renderer/basic.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'renderVueComponentToString',
    plugins: [node(), cjs()]
  },
  'web-server-renderer-webpack-server-plugin': {
    entry: resolve('server/webpack-plugin/server.js'),
    dest: resolve('packages/vue-server-renderer/server-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-webpack-client-plugin': {
    entry: resolve('server/webpack-plugin/client.js'),
    dest: resolve('packages/vue-server-renderer/client-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  // 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]
  },
  // Weex runtime framework (CommonJS).
  'weex-framework': {
    weex: true,
    entry: resolve('weex/entry-framework.js'),
    dest: resolve('packages/weex-vue-framework/index.js'),
    format: 'cjs'
  },
  // Weex compiler (CommonJS). Used by Weex's Webpack loader.
  'weex-compiler': {
    weex: true,
    entry: resolve('weex/entry-compiler.js'),
    dest: resolve('packages/weex-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
  }
}

function genConfig (name) {   ///  主要是通過我們上面的builds對應到rollup格式的一個轉換,如把entry轉換為input
  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'
    }
  }

  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
}

if (process.env.TARGET) {  //  拿到使用者環境資訊中的TARGET
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)   
  //  取到builds下的所有索引,然後遍歷執行genConfig方法
}
複製程式碼

下面我們開始最整個打包編譯進行梳理

vue原始碼解析之npm run build發生了什麼?

當我們執行build檔案時,在引入模組之後,會在./config中拿到builds,builds是什麼呢?

在config檔案下的尾部有這樣一段程式碼

vue原始碼解析之npm run build發生了什麼?

我們可以清晰的看到該在最後一行對builds拿到了所以的keys進行了一個遍歷,執行genConfig方法

vue原始碼解析之npm run build發生了什麼?

通過上圖可以看出來,builds是含有一個個檔案資訊的物件,相當於是對rollup引數的一個對映,其中entry為入口,對應rollup的input,dest為出口,對應rollup的output ,format為格式,banner為版本註釋

這個物件通過key傳給了genConfig方法,genConfig又是什麼?我們看一下:

vue原始碼解析之npm run build發生了什麼?

不難看出,genConfig方法就是一個將builds物件轉化為rollup使用的格式的方法,就這樣含有rollup資訊的格式,吐給了builds檔案中的變數builds,也就是我們最開始提到的。

然後我們接著看build檔案的程式碼

接下來通過拿到process.argv[2],進行了一個過濾,過濾掉不需要編譯的檔案,註釋說的已經夠詳細了,不想在過多解釋了。

vue原始碼解析之npm run build發生了什麼?

接下來會執行build()方法,可以看的出來,build方法是一個迴圈,迴圈執行buildEntry()方法

vue原始碼解析之npm run build發生了什麼?

buildEntry則是真正的開始執行rollup,對返回的builds進行編譯,然後生成對應的檔案。

就這樣整個編譯過程就執行完了,覺得有幫助的,點個小心心吧!

相關文章