Vue.js 源码构建

关键词:看住build.js文件 定义依赖 拿到配置 进行筛选 进行构建

Vue.js 源码构建

Vue 用 Rollup 做构建

Rollup 和 webpack 一样都是构建工具,webpack 更强大,rollup 更适合 JavaScript 库的编译且更轻量且代码更友好,所以 Vue.js 选了 rollup 做构建,它的构建相关配置都在 scripts 目录下。

构建脚本

vue 发布在 npm 上,每个 npm 包(相当于一个项目)都需要一个package.json文件来对他做描述,它的内容实际上是一个标准的 JSON 对象。我们通常会配置script字段作为 NPM 的执行脚本。

{
    
    
  "name": "vue",
  "version": "2.6.13",
  "main": "dist/vue.runtime.common.js",
  "module": "dist/vue.runtime.esm.js",
  "scripts": {
    
    
    // 构建相关的任务就是这三个, "build" 是建立 web平台的 Vue.js,"build ssr" 是输出跟 server renderer 相关,"build weex" 是跟 weex 相关。作用都是构建 Vue.js,后面 2 条是在第 1 条命令的基础上,添加一些环境参数。
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
  }
}
字段 含义
name 名字,是唯一的
version 版本
main npm 包入口,import "vue"时会通过 main 查找入口
module module 和 main 非常类似,在 webpack2 以上是把 module 做默认入口,也可以说 Vue.js 的默认入口是 esm.js 文件
scripts npm 提供了 npm scripts , scripts定义了很多脚本,每个脚本都是一个任务,通过 npm run 值(dev,bulid)可以进行各种不同的脚本执行不同的任务。

通过构建生成的目标代码在 dist 文件下,默认已经构建了很多版本的 Vue.js 。

构建过程

为什么可以构建如此多版本的 Vue.js ?那就要先了解它的构建过程。例如执行npm run build,实际上是执行了node scripts/build.js脚本,也就是跑了 scripts 文件夹下build.js文件的 js 。

(1)定义依赖的模块

// 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)从config.js配置文件中拿到构建所需要的所有的配置

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

config.js文件底部暴露了一个方法exports.getAllBuilds(),这是一个函数,通过Object.keys(builds)拿到一个数组,然后通过map()方法调用genConfig()函数

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

1、Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组

2、枚举是指一一列举列

3、map的三个参数:数组元素,元素索引,原数组本身arr.map((currentValue,index,array)=>{})

builds

其实builds是一个对象,每个key对应一个对象,其实就是不同版本 Vue.js 编译的配置

entry:入口,通过resolve()函数传字符串web/entry-runtime.js
dest:目标
format:文件格式,通过不同的format可以构建不同的版本
banner:局部变量,定义注释

entrydest

所以最后以entry-runtime.js为编译入口,最后生成vue.runtime.common.dev.js文件

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

resolve()函数接收一个参数,在entrybase/前面第一个值webp.slice(base.length + 1)返回entry-runtime.js

destbase取值dist,在alias.js里没有dist,于是直接返回当前目录(__dirname)的大目录('../'),找到vue.runtime.common.dev.js文件

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

aliases()函数在alias.js文件最后导出一个对象,对象里有很多key,通过resolve()函数返回字符串,字符串是一个目录,resolve()调用了path.resolve()path.resolve()是node.js提供的一个路径解析的方法,__dirname是指当前的目录,并往上去找一级,找到大目录,再传参数p

因此alias.js文件是提供了一个到最终真实文件地址的一个映射关系

// 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

format属性表示构建的格式,cjs表示构建出来的文件遵循 CommonJS 规范,es表示构建出来的文件遵循 ES Module 规范。umd表示构建出来的文件遵循 UMD 规范。

// 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

可以知道版本、创建者、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()

回到scripts/cofig.js中,数组调用genConfig()函数:
optsgenConfig获取keyoptsbuildskey对应的对象
config:构造一个新的config对象,这个对象的数据结构才是真正的 Rollup 对应的配置结构,entry只是我们自己定义的entry,但是在 Rollup 中是叫input。所以config是 Rollup 最终要的配置。

总的来说就是我们将builds经过映射和转化生成 Rollup 最终需要的配置,他也是一个数组。最终生成的数组返回到build.js文件下。

// 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)对配置进行过滤,过滤掉我们不需要的,就剩下我们需要编译的这样就可以构建出不同用途的 Vue.js 了

process.argv[2]对应的是package.json文件下的-- weex-- web,如果有这些参数就会通过filter把之前不需要打包的过滤掉,如果没有参数就把weex给过滤掉。

// 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)调用build()函数做真正的构建过程

build()函数定义了一个next()方法,在next()方法执行的时候调用buildEntry()以及计数器built

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

buildEntry()拿到了buildsconfigconfig就是 Rollup 最终编译需要的configconfig编译完就拿到了bundlebundle通过generate()产生outputoutput对应生成的目标。可能会对code进行修改,比如判断是否要压缩的js,isProd的定义是文件以min.js结尾,就再做一次terser.minify压缩,最终调用write()方法最终生成到 dist 目录下。

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

个人理解:就是 执行代码和定义代码一般写在 js 文件的上面,方便看执行过程,所以 function 一般写在 js 文件的后面部分。

猜你喜欢

转载自blog.csdn.net/qq_44997147/article/details/121589395