头图

rollup入门以及vue使用rollup构建源码分析

清水

rollup简介

首先,rollup.js是一个JavaScript 模块打包器
可以将我们自己编写的js代码与第三方模块打包在一起,也可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
rollup直接支持tree shaking只有ES模块才支持,在打包构建时,会对编译的代码进行静态分析,打包结果只包含使用到的代码,这样可以大幅精简代码量。
和我们熟悉的webpack相比,它更专注于js类库打包,而webpack更偏向于应用打包。

Rollup基本应用

从0使用Rollup实现一个基础使用案例, 全程使用npm操作,yarn同理(论团队统一npm和yarn以及对应版本统一的重要性

初始化操作

npm i rollup -g
mkdir roolup-demo
cd roolup-demo
npm init
mkdir src
vim src/name.js
// i 输入
const name = 'Ada'
export default name
// esc :wq
vim src/main.js
// 输入
import name from './name.js'
export default function() {
  console.log(name)
}

预览打包后源码

rollup src/main.js -f es

输出

image.png

解释一下命令参数
-f --format的缩写,代表生成代码的格式,amd就是AMD标准,cjs是commonjs,es就是es标准

下面我们输出一下构建后的文件

rollup src/main.js -f es -o dist/output.js

-o 等同output.file

输出文件:
image.png

再来输出一个commonjs风格文件

rollup src/main.js --format cjs --o dist/output-cjs.js

image.png

test

$ node
> const fn = require('./dist/index-es.js')
> fn()
Ada

拓展node 不支持ES标准 如何处理

所以默认以上代码使用node执行es版本会报错,我们可以使用babel-node解决

安装如下

npm i @babel/core @babel/node @babel/cli -g

然后创建babel的配置文件.babelrc

touch .babelrc
// 内容
{
  "presets": ["@babel/preset-env"]
}

安装依赖

npm i -D @babel/core @babel/preset-env

然后通过babel编译代码

babel dist/output.js

$ babel-node 
> require('./dist/output.js')
{ default: [Function: main] }
> require('./dist/output.js').default()
Ada

这里babel认为default是function name

下面再介绍一下rollup.js配置文件

// 创建配置文件
touch rollup.config.js

内容如下

export default {
  input: './src/main.js',
  output: [{
    file: './dist/index-cjs.js',
    format: 'cjs',
    banner: '// cjs banner',
    footer: '// cjs footer'
  }, {
    file: './dist/index-es.js',
    format: 'es',
    banner: '// es test banner',
    footer: '// es test footer'
  }]
}

rollup的配置文件说明

rollup的配置文件需要采用ES模块标准编写
input表示入口文件的路径

output表示输出文件的内容:

  • output.file:输出文件的路径(老版本为dest,已经废弃)
  • output.format:输出文件的格式
  • output.banner:文件头部添加的内容
  • output.footer:文件末尾添加的内容

有了配置文件就可以直接使用rollup -c进行打包了

最后我们来实现一个简单地build代码

npm i -D rollup
touch rollup-build.js
touch rollup-input.js
touch rollup-output.js
// 内容
// input
module.exports = {
  input: './src/main.js'
}
// output
module.exports = [{
  file: './dist/index-cjs.js',
  format: 'cjs',
  banner: '// hello',
  footer: '// bye'
},
{
  file: './dist/index-es.js',
  format: 'es',
  banner: '// hello A',
  footer: '// bye A'
},{
  file: './dist/index-amd.js',
  format: 'amd',
  banner: '// hello B',
  footer: '// bye B'
},
{
  file: './dist/index-umd.js',
  format: 'umd',
  name: 'umd',
  banner: '// hello C',
  footer: '// bye C'
}]
// build 
// 通过rollup.rollup(input)获取输入
// 通过bundle.write(output)输出文件
const rollup = require('rollup')
const inputOptions = require('./rollup-input')
const outputOptions = require('./rollup-output')
async function rollupBuild(input, output) {
  const bundle = await rollup.rollup(input) // 根据input配置进行打包
  console.log(`current:${output.file}`)
  await bundle.write(output) // 根据output配置输出文件
  console.log(`${output.file} success!`)
}

(async function () {
  for (let i = 0; i < outputOptions.length; i++) {
    await rollupBuild(inputOptions, outputOptions[i])
  }
})()

执行测试一下

node rollup-build.js

image.png

基础拓展:watch 监听文件变化自动build

touch rollup-watch-build.js
touch rollup-watch.js
module.exports = {
  include: 'src/**', // 监听的文件夹
  exclude: 'node_modules/**' // 排除监听的文件夹
}

watch build

const rollup = require('rollup')
const inputOptions = require('./rollup-input')
const outputOptions = require('./rollup-output')
const watchOptions = require('./rollup-watch')

const options = {
  ...inputOptions,
  output: outputOptions,
  watchOptions
}

const watcher = rollup.watch(options) // 调用rollup的api启动监听

watcher.on('event', event => {
  console.log('重新打包...', event.code)
})

这样当我们修改src目录文件后就会自动重新打包
平时我们也可以脚本直接写 rollup -wc 命令

rollup插件扫盲 方便阅读vue构建源码

插件描述
resolve插件集成外部模块
commonjs插件支持CommonJS模块
babel插件编译ES6语法,兼容低版本浏览器
commonjs插件支持CommonJS模块
uglify插件代码最小化打包
json插件支持json

插件的使用都很简单,大家可以按需进行尝试
下面我们进入vue build部分,去分析一下在vue源码中build做了那些事。

vue build 分析

直接从源码入手,以下代码核心部分已增添对应备注。

1.首先将下载源码到本地安装依赖

git clone https://github.com/vuejs/vue.git"
cd vuejs
npm i

2.我们来看一下 package.json 我们当前需要关心的部分

"scripts": {
  "build": "node scripts/build.js",
  "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
  "build:weex": "npm run build -- weex"
  ...
},
"devDependencies": {
  "rollup": "^1.0.0",
  "rollup-plugin-alias": "^1.3.1",
  "rollup-plugin-buble": "^0.19.6",
  "rollup-plugin-commonjs": "^9.2.0",
  "rollup-plugin-flow-no-whitespace": "^1.0.0",
  "rollup-plugin-node-resolve": "^4.0.0",
  "rollup-plugin-replace": "^2.0.0",
  ...
}

由依赖可以看出vue有使用rollup

下面我们根据 build scripts去看一下构建脚本都做了什么事

3.scripts/build.js

注释已经加入源码之中 我们直接来看源码 一个常规vue源码构建我们主要看以下三个文件就可以 build.js config.js alias.js

build 执行大致可以分为以下5部分

image.png

以下为build.js 源码请结合注释进行查看

const fs = require('fs') // 文件处理
const path = require('path') // 本地路径解析
const zlib = require('zlib') // gzip压缩
const rollup = require('rollup') // 打包工具
const terser = require('terser') // 代码压缩

// 1、创建dist目录
// 判断dist 是否存在 不存在进行创建
if (!fs.existsSync('dist')) {
  fs.mkdirSync('dist')
}

// 2、通过 config 生成rollup配置
let builds = require('./config').getAllBuilds()

// 3、rollup配置文件过滤。
// 根据传入的参数,对rollup配置文件的内容进行过滤,排除不必要的打包项目
// filter builds via command line arg
console.log('process.argv', red(process.argv))
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  console.log('filters', red(filters))
  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.rollup打包
build(builds)

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

  next()
}

// 打包执行核心函数
function buildEntry (config) {
  // 获取rollup 配置信息
  const output = config.output
  const { file, banner } = output
  const isProd = /(min|prod)\.js$/.test(file) //是否压缩 min结尾 标识
  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)
      }
    })
}
// 5. 文件输出 zlib.gzip压缩
// dest 路径 code源码
function write (dest, code, zip) {
  return new Promise((resolve, reject) => {
    function report (extra) {
      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)
}
// 生成蓝色文本 ANSI 转义码 \x1b[(文字装饰);(颜色代码):
function blue (str) {
  return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
}
// 同理我们实现一个 输入红色文本
function red (str) {
  return '\x1B[31m' + str + '\x1B[39m'
}

image.png

config.js 选取部分内容

const path = require('path') // 本地路径解析
const buble = require('rollup-plugin-buble') // es6+ 语法转为es5
const alias = require('rollup-plugin-alias') // 替换模块别名
const cjs = require('rollup-plugin-commonjs') // 支持commonjs模块
const replace = require('rollup-plugin-replace') // 替换代码中变量为指定值
const node = require('rollup-plugin-node-resolve') //外部模块集成
const flow = require('rollup-plugin-flow-no-whitespace') // 去除flow静态类型检查
const version = process.env.VERSION || require('../package.json').version // 获取当前版本
const weexVersion = process.env.WEEX_VERSION || require('../packages/weex-vue-framework/package.json').version
// weex版本 Weex 是一款轻量级的移动端跨平台动态性技术解决方案 The Weex podling retired on 2021-05-14
const featureFlags = require('./feature-flags') // 添加2个构建常量
// 打包后 banner
const banner =
  '/*!\n' +
  ` * Vue.js v${version}\n` +
  ` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
  ' * Released under the MIT License.\n' +
  ' */'

// 打包weex-factory使用
const weexFactoryPlugin = {
  intro () {
    return 'module.exports = function weexFactory (exports, document) {'
  },
  outro () {
    return '}'
  }
}

const aliases = require('./alias') // 别名及其对应的绝对路径
const resolve = p => {
  const base = p.split('/')[0]
  // aliases 匹配 name是否存在
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    // 不存在则合并根路径和传入
    return path.resolve(__dirname, '../', p)
  }
}
// build 配置
const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  },
  'web-runtime-cjs-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.prod.js'),
    format: 'cjs',
    env: 'production',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.dev.js'),
    format: 'cjs',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  'web-full-cjs-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.prod.js'),
    format: 'cjs',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  ...
}

// 根据name生成对应环境的rollup配置
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)
      }
    }
  }

  // built-in vars
  const vars = {
    __WEEX__: !!opts.weex,
    __WEEX_VERSION__: weexVersion,
    __VERSION__: version
  }
  // feature flags
  Object.keys(featureFlags).forEach(key => {
    vars[`process.env.${key}`] = featureFlags[key]
  })
  // build-specific env
  if (opts.env) {
    vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)
  }
  config.plugins.push(replace(vars))

  if (opts.transpile !== false) {
    config.plugins.push(buble())
  }

  Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })
  // console.log('vars', vars)
  // console.log('config', JSON.stringify(config, null, 2))
  return config
}
// 判断环境变量TARGET是否定义 存在即输出
if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

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'),
  sfc: resolve('src/sfc')
}

以上就是本次分享的内容,希望大家可以一步一步跟着去操作,进行实践。

阅读 91

清水圈
闲鱼的爬坑之路、、、

清水既心、

26 声望
2 粉丝
0 条评论
你知道吗?

清水既心、

26 声望
2 粉丝
宣传栏