rollup
安装
pnpm add rollup命令操作
定义一个main.js文件用来做打包测试,在package.json中添加打包测试
const random = () => {
return Math.random().toString(36).substr(2);
};
console.log('1 =====', random());
console.log('2 =====', random());{
"scripts": {
"build:rollup": "rollup ./scripts/main.ts --file ./dist/main.js --format iife"
}
}- 第一个参数表示入口文件
--file表示输出文件--format表示输出格式:amd、cjs、es、iife、umd、system--config表示指定配置文件路径--watch监听文件变化,自动打包
配置文件打包
在命令中添加--config参数,指定配置文件路径rollup.config.js,之后通过rollup --config rollup.config.js进行打包
export default {
// 入口文件
input: './scripts/main.ts',
output: {
// 单入口打包只需要指定file,不需要指定dir(多入口打包需要指定dir)
// dir: 'dist',
file: './dist/bundle.js',
format: 'iife',
name: 'modify',
piugins: []
},
plugins: [
// 插件
]
};插件
通过Babel对高级语法进行转换
安装这个 @rollup/plugin-babel插件 ,同时需要安装babel的相关插件,前面的入口文件当中使用到了箭头函数,接下来使用这个插件看打包结果是否会转换为普通函数
pnpm -Dw add @rollup/plugin-babel
pnpm -Dw add @babel/core @babel/plugin-transform-runtime @babel/runtime @babel/preset-env先配置babel,添加babel.config.js文件,之后修改rollup.config.js文件,添加@rollup/plugin-babel 插件,如果打包入口是ts文件,需要添加extensions参数,把ts加上
- 单入口打包指定file,多入口打包需要指定dir
- 其中多入口打包后的文件名与设置的key一致【如下
main.ts的key为index,打包后的文件名为index.js】
export default {
presets: ['@babel/preset-env'],
plugins: [
'@babel/plugin-transform-runtime'
]
};import {babel} from '@rollup/plugin-babel';
export default {
// 单入口打包
// input: './scripts/main.ts',
input: {
index: './scripts/main.ts'
},
output: {
// 单入口用这个指定打包文件名
// file: './dist/bundle.js',
// 多入口用这个指定打包目录
dir: 'dist',
format: 'iife',
name: 'modify'
},
plugins: [babel({babelHelpers: 'runtime', exclude: 'node_modules/**', extensions: ['.js', '.jsx', 'ts', '.tsx']})]
};(function () {
'use strict';
var random = function random() {
return Math.random().toString(36).substr(2);
};
console.log('1 =====', random());
console.log('2 =====', random());
})();清除之前的构建文件
在进行多入库构建时,修改key之后进行重新打包,之前的构建文件会保留,我修改了key之后并不想保留之前的构建文件。本质上就是通过node脚本把这个dist先删掉。
安装插件 rollup-plugin-clear 修改rollup.config.js 文件,和添加babel插件一样,其中targets参数指定要删除的目录,watch参数指定是否监听文件变化,自动删除指定目录下的文件,使用这个需要在执行命令的时候添加--watch 参数
pnpm -Dw add rollup-plugin-clearimport clear from 'rollup-plugin-clear';
export default {
plugins: [
clear({
targets: ['dist'],
watch: true
})
]
};路径别名
pnpm -Dw add @rollup/plugin-aliasimport alias from '@rollup/plugin-alias';
export default {
plugins: [
// 路径别名
alias({
entries: [
{find: '@', replacement: './src'},
]
})
]
};json解析
使用这个插件@rollup/plugin-json
代码压缩
在打包之后会打包后的代码进行一个压缩,安装这个插件@rollup/plugin-terser
pnpm -Dw add @rollup/plugin-terser只需要修改output选项添加plugins: [terser()] 即可,这里output可以是数组,每个元素表示一个打包输出,每个元素可以添加不同的配置,比如压缩和不压缩分别打包到不同的目录下,
注意点:因为入口文件是一个但是想打包出压缩和不压缩两个版本,就需要指定不同的文件名,使用entryFileNames 来指定文件名
import terser from '@rollup/plugin-terser';
export default {
output: [{
dir: 'dist',
format: 'iife',
name: 'modify',
entryFileNames: '[name].min.js',
plugins: [terser()]
}, {
dir: 'dist',
format: 'iife',
name: 'modify',
entryFileNames: '[name].js'
}],
};生成html文件
安装这个插件rollup-plugin-generate-html-template
pnpm -Dw add rollup-plugin-generate-html-template把这个插件加上到plugins中,并添加相应的配置,需要指定模版文件和输出文件,之后再进行打包会发现dist当中会生成一个html文件,这个文件会自动注入打包结果
import htmlTemplate from 'rollup-plugin-generate-html-template';
export default {
plugins: [
// 根据模版生成 html,自动注入打包结果
htmlTemplate({
// 模版文件位置
template: './scripts/index.html',
// 和output.dir指定的目录一致
target: 'index.html'
})
]
};但是当打包的类型是esm的时候,这个时候再运行html,会发现报错Uncaught SyntaxError: Unexpected token 'export' (at index.min.js:1:117),是因为HTML中的script标签默认加载的是传统脚本(classic script),无法直接使用 import/export 语法。需要手动给script标签加上type="module",
在这个插件当中可以添加属性attrs: ['type="module"'],这个会默认加到script标签中,async、defer
打包第三方依赖
在入库js当中引入vue定义一个响应式数据,这个时候执行打包,会报错Uncaught TypeError: Failed to resolve module specifier "vue". Relative references must start with either "/", "./", or "../",这是因为vue这个依赖没有打包进去
安装插件@rollup/plugin-node-resolve
pnpm -Dw add @rollup/plugin-node-resolve使用这个插件,只需要添加这个插件到plugins中,就可以打包第三方依赖了
import {nodeResolve} from '@rollup/plugin-node-resolve';
export default {
plugins: [nodeResolve()]
};打包之后去查看代码,可以发现是将vue打包进去了,但是运行起来之后会发现里面有process去判断环境的代码,这个node下才有的变量,在浏览器当中是运行不了的。
字符替换
为了解决上面的问题,就需要使用这个插件@rollup/plugin-replace
pnpm -Dw add @rollup/plugin-replace使用插件,参数是一个对象,里面可以添加多个替换的参数
- 参数的key是替换的key,value是替换的值
- 如果value是一个函数,那么这个函数会返回替换的值,这个函数会接收一个参数,这个参数是替换的key,这个函数返回的值会替换掉key的值。
import replace from '@rollup/plugin-replace';
export default {
plugins: [
// 替换打包结果中的关键词(浏览器不兼容)
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
__buildDate__: () => JSON.stringify(new Date()),
__buildVersion: 15,
// 防止替换字符串后跟单个等号
preventAssignment: true
})
]
};启动静态服务器
安装插件 rollup-plugin-serve
这个插件在修改vue文件之后不会刷新页面,可以安装rollup-plugin-live-server 插件,这个插件也不是做热替换,是直接自动帮你刷新了一下页面
pnpm -Dw add rollup-plugin-serve使用插件,参数是一个对象,里面可以添加多个配置项,等执行打包之后在dist目录下启动一个静态服务器
open:是否自动打开浏览器contentBase:指定静态资源的目录openPage:指定打开的页面host:指定主机名port:指定端口号
import serve from 'rollup-plugin-serve';
export default {
plugins: [
serve({
open: true,
contentBase: 'dist',
openPage: '/index.html',
host: 'localhost',
port: 10000
})
]
};处理vue单文件组件
处理模版
现在我是一个vue项目,然后我想通过rollup对这个主入口main.js进行打包,这肯定是会报错的[!] RollupError: scripts/App.vue (1:0): Expression expected (Note that you need plugins to import files that are not JavaScript)
import {createApp} from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');在webpack当中对vue打包需要添加对应的loader,在这里也需要安装插件rollup-plugin-vue
pnpm -Dw add rollup-plugin-vue使用插件,现在vue单文件组件lang设置为ts还是报错,先暂时设置为js
import vuePlugin from 'rollup-plugin-vue';
export default {
plugins: [vuePlugin()]
};处理样式
安装插件rollup-plugin-postcss,这个插件可以用来处理css,
pnpm -Dw add rollup-plugin-postcss插件使用,其中可以设置extract属性,这个属性可以指定是否将样式抽离到单独的文件中,如果设置为true ,那么就会将样式抽离到单独的文件中,如果设置为false,那么就会将样式内联到html中,如果设置为string,那么就会将样式抽离到指定的文件中
import postcss from 'rollup-plugin-postcss';
export default {
plugins: [
postcss({
// 把样式抽离到单独的文件中
extract: true
// extract: 'dist/my-custom-file-name.css'
})
]
};当然了不单单只是处理css,还可以对css进行压缩和添加兼容性。安装插件依赖autoprefixer和cssnano ,这两个插件是用来处理css的,autoprefixer是用来添加浏览器前缀的,cssnano是用来压缩css的
pnpm -Dw add autoprefixer cssnano- cssnano 配置
preset:指定使用的预设,默认是default,可以设置为advanced,这个会开启更多的压缩选项
- autoprefixer 使用
使用插件,这个是添加到postcss当中的plugins当中的,同时需要在package.json文件中添加一个browserslist属性,用来指定浏览器的兼容性。
import postcss from 'rollup-plugin-postcss';
import cssnano from 'cssnano';
import autoprefixer from 'autoprefixer';
export default {
plugins: [
postcss({
// 把样式抽离到单独的文件中
extract: true,
// extract: 'dist/my-custom-file-name.css',
plugins: [cssnano(), autoprefixer()]
})
]
};{
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version",
"last 1 ie version"
]
}
}如果使用到了scss或者less,那么就需要安装对应的编译器sass、node-sass、less
环境变量设置
在windows中设置环境变量,可以使用set命令,但是这个命令只能设置临时环境变量,如果需要设置全局环境变量,可以使用setx命令
"build": "set NODE_ENV=production&& rollup -c rollup.config.js",cross-env这个插件是为了解决运行跨平台设置和使用环境变量的脚本
pnpm -Dw add cross-env那么build脚本就可以改写成这样
"build:cross-env": "cross-env NODE_ENV=production rollup -c rollup.config.js",设置这个环境变量主要是为了在rollup.config.js文件中使用process.env.NODE_ENV这个变量,现在这个变量的值就是production ,就可以通过这个变量来控制在生产环境下要使用那些插件来进行打包,比如terser 插件,这个插件可以压缩代码,但是这个插件只在生产环境下使用,所以就可以通过这个变量来判断是否使用这个插件
引入CDN
在前面我们一个vue项目,只编写了一个响应式变量进行打包,打包体积就很大了,这显然不是我们想要的,接下来就引入CDN进行打包,减少打包体积
首先在rollup.config.js当中添加配置,external属性可以指定哪些包需要被排除,在output当中添加一个globals 属性,这个属性的值是一个对象,对象中的键是包名,值是包在打包后的全局变量名,
globals中配置的为什么是‘Vue’? 通过CDN使用vue 之前看官方文档确实没注意到哈,后面所有通过CDN引入的都可以去对应官网找CDN配置
export default {
output: [
{
dir: 'dist',
format: 'esm',
entryFileNames: '[name].min.js',
plugins: [isProduction ? terser() : ''],
globals: {vue: 'Vue'}
}
],
// 排除包
external: ['vue']
};同时修改模版文件,在模版文件当中引入CDN,需要注意的是如果打包后的文件是es modules,那么需要使用es modules的引入方式,如果打包后的文件是iife,那么需要使用iife的引入方式
<!--iife-->
<script src="https://unpkg.com/vue@3.5.22/dist/vue.global.js"></script>
<!--es modules-->
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3.5.22/+esm"
}
}
</script>当使用异步导入的时候,rollup会默认进行拆分(比方说:vue-router的路由当中用异步导入组件的时候再进行构建),iife是不支持拆分的,这个时候就会报错[!] RollupError: Invalid value "iife" for option "output.format" - UMD and IIFE output formats are not supported for code-splitting builds.
模块拆分
使用到rollup的output.manualChunks配置
- 对象,对象中的键是模块名,值是模块名对应的模块
- 函数,函数的参数是模块的id,返回值是模块名
同时加上chunkFileNames属性,这个属性对代码分割中产生的 chunk 自定义命名
export default {
output: [
{
// 模块拆分
// manualChunks: {
// 'vue': ['vue'],
// 'vue-router': ['vue-router']
// }
manualChunks: (id) => {
if (id.includes('node_modules')) {
return 'vendor';
}
return null;
},
// 静态文件命名
assetFileNames: '[name]-[hash][extname]',
// chunk分包文件命名
chunkFileNames: '[name]-[format].js'
}
]
};sourcemap
如果该选项值为 true,那么将生成一个独立的 sourcemap 文件。如果值为 "inline",那么 sourcemap 会以 data URI 的形式附加到 output 文件末尾。如果值为 "hidden",那么它的表现和 true 相同,除了 bundle 文件中将没有 sourcemap 的注释。
开发插件
构建钩子
定义一个js文件作为rollup的插件,其中rollup插件钩子 就是这个js文件当中的方法,这些方法会在rollup打包的过程中被调用
function build() {
return {
name: 'rollup-plugin-build',
version: '0.0.1',
options() {
console.log('options');
},
buildStart(inputOptions) {
console.log('buildStart');
},
buildEnd() {
console.log('buildEnd');
}
};
}
export default build;polyFill插件
现在想在每一个入口文件当中引入polyfill,那么就需要定义一个插件,这个插件会自动在每一个入口文件当中引入polyfill,
- 实现一个
resolveId钩子,先判断是否为入口点,如果是入口点,那么就返回一个对象,对象中包含模块的id,以及是否为外部模块 - 实现一个
load钩子,先判断是否为polyfill模块,如果是,那么就返回polyfill的代码,否则就返回null PROXY_SUFFIX用于标识是否需要引入polyfillPOLYFILL_ID用于标识polyfill模块的id- this.resolve 方法用于解析模块 ID,它返回一个包含模块信息的对象
- this.load 方法用于加载模块的内容,它返回一个包含模块内容的对象
- 整体的流程是:
resolveId -> load -> resolveId -> load
const PROXY_SUFFIX = '?inject-polyfill';
const POLYFILL_ID = '\0polyfill';
function polyfill() {
return {
name: 'rollup-plugin-polyfill',
version: '0.0.1',
async resolveId(source, importer, options) {
if (source === POLYFILL_ID) {
return {id: POLYFILL_ID, moduleSideEffects: true};
}
// 说明这是一个入口点
if (options.isEntry) {
// PluginContext.resolve 方法用于解析模块 ID,它返回一个包含模块信息的对象。
// 该对象包含模块的 ID、路径、是否为外部模块等信息。
const resolution = await this.resolve(source, importer, {skipSelf: true, ...options});
console.log('resolveId', resolution);
// 如果该模块无法解析,或者是外部模块,直接返回,rollup会进行external提示
if (!resolution || resolution.external) {
return resolution;
}
// 加载模块内容
const moduleInfo = await this.load(resolution);
console.log('load', moduleInfo);
// 模块有副作用,不要 tree-shaking
moduleInfo.moduleSideEffects = true;
return `${resolution.id}${PROXY_SUFFIX}`;
}
return null;
},
async load(id) {
if (id === POLYFILL_ID) {
return 'console.log("polyfill")';
}
// 判断是不是从resolveId()返回的代理 ID
if (id.endsWith(PROXY_SUFFIX)) {
const entryId = id.slice(0, -PROXY_SUFFIX.length);
let code = `
import ${JSON.stringify(POLYFILL_ID)}';
export * from ${JSON.stringify(entryId)};
`;
// 如果钩子有返回值,不去走后面的load()钩子,也不会读硬盘上的文件
return code;
}
return null;
}
};
}