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

输出
rollup入门以及vue使用rollup构建源码分析
文章图片

解释一下命令参数
-f --format的缩写,代表生成代码的格式,amd就是AMD标准,cjs是commonjs,es就是es标准
下面我们输出一下构建后的文件
rollup src/main.js -f es -o dist/output.js

-o 等同output.file
输出文件:
rollup入门以及vue使用rollup构建源码分析
文章图片

再来输出一个commonjs风格文件
rollup src/main.js --format cjs --o dist/output-cjs.js

rollup入门以及vue使用rollup构建源码分析
文章图片

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入门以及vue使用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

rollup入门以及vue使用rollup构建源码分析
文章图片

基础拓展: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部分 rollup入门以及vue使用rollup构建源码分析
文章图片

以下为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' }

rollup入门以及vue使用rollup构建源码分析
文章图片

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

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

    推荐阅读