引言
-
前端基建工程化"横行",专注于业务代码得不到提升,是不是觉得空虚觉得寂寞觉得冷?
-
很多人想写个自己的库,但是没有方法,光有技术却得不到施展,总是在构建上遇到阻碍。
-
每次构建的时候发现流行的库都用上了高级的构建技巧,自己还在用vue-cli的 vue-cli-service 通用打包,看起来不太高大上?(这里没有踩vue-cli的意思,vue-cli 封装了所有类型资源的打包规则,一键打包其实非常方便了,一般的库和组件都不需要二次定制,特别是现在的vite2.0内置的打包配置也足够开箱即用了,非常方便)
这篇文章旨在分享如何创建monorepo/packages
项目管理方式,基于rollup
打包ts代码生成dts声明文件,采用自定义脚本一键支持打包,最小化更改(rename)即可挪为已用。目前已经配置好了基础打包和代码引入,如无特殊需求,更名后即可食用(后期可能会写个脚手架, ts-monorepo-starter什么的)。
转载声明
本文以分享为目的的文章,不存在任何利益行为。署名内容来源本文,或结尾声明参考文章链接即可任意使用本文内容,包括转载和复制修改文章任何内容,无需告知我。
yarn workspace
其实早在yarn 1.0 就支持了workspaces,因为各大仓库都先后去拥抱了workspaces,比如react, babel,现在的vue3.0,后面重构的vite2等,从实践上证明了yarn workspaces的优秀。
使用yarn workspace 的好处
当然,一键yarn install
只是表面上的好处;内在的好处大致以下3点:
- 你的所有的库相互之间会被
SymbolLink到一起
,举个例子,vite2.0没重构使用workspace之前,调试的时候需要单独在vite的rootdir下使用 yarn link创建一个全局的 vite Symbollink, 然后在playground里,再yarn link vite
,就可以使用vite binary
anywhere,使用了workspace之后,你可以直接在packages下创建一个private: true
的调试项目,依赖里写入vite,yarn install就可直接创建SymbolLink非常方便。而且,每个包都是相互link。 - 每个包安装的依赖都会安装到rootdir的node_modules目录里,这样可以让yarn自动的优化这些包,处理版本问题和依赖问题。
- 使用一个lockfile即可控制版本,通用的库可以
yarn add -W **
安装的workspace公共区域,私有的库可以yarn workspace project-1 add package -D
往packages/project-1项目里安装依赖package。那么公用的库可以保持版本一致,私有的库可以各自安装互不影响,完美。
记得第一次调试vuetify的时候不知道有workspace的概念,更不知道lerna这个东西,doc里安装依赖,组件库里安装一下,简直头都要炸了,现在回想,真特么傻了。 这样的结构方便找代码,依赖清晰,库之间引用清晰,香。
快速创建workspace
packages
|--project1
| |--package.json
|--project2
| |--package.json
package.json
首先们创建一个这样的目录,使用yarn init -y
快速的创建。
- 在rootdir的package.json 里设置
private: true
,并设置你的package目录
因为跟目录的package.json 只是用来管理依赖的,所以其他信息已经没有用了,可以随意删除。
{
+ "private": true,
- "name": "rootdir",
"main": "index.js",
+ "workspaces": [
+ "packages/*"
+ ]
}
worksapces/packages
下存放的就是我们要开发的库,如上的project1
和project2
就这么简单,当你在命令行敲下yarn
或yarn install
的时候,包之间的依赖就创建成功了。剩下的只需要按
yarn workspace <workspace_name> <command>
和 yarn add <package-name> <command> -W
这样就可以安装库或者全局公用的依赖了。
更多的workspace相关的命令行就不在这里赘述了,甚至于你使用lerna也可以,对后面没有任何影响。
rollup
作为打包工具,目前最流行的依旧是webpack 和 rollup。webpack作为老牌打包工具,从早玩到晚,那肯定腻了,我采用的是rollup,一是因为rollup配置非常简单,二是rollup打包下来gzip后总能比webpack小那么一丢丢,也是很神奇。另外一个不算原因的原因是此处的playground采用尤雨溪新开发的vite用来起调试服务,那就和vite使用相同的打包工具就是(vite重构后已经使用esbuild处理代码,使用了rollup-like的api,兼容一些rollup插件,我特么好家伙,这是要抢webpack的饭碗,也要抢rollup的筷子)。
此次配置要达到的目的
- 支持所有库一键打包各自需要支持的平台(umd, esm, iiff, cjs...)
- 支持单个库监听修改并编译文件
- 支持生成dts文件,支持生成ts type doc
- 支持打包脚本无痛转移,今后只需要复制代码就可以用
不会详细介绍的内容: eslint、prettier代码格式化,单元测试,ts type check,与流程内无关的命令行等,保证需要这部分内容的同学能快速获取,其它内容全网已经有很多教程就不赘述了。
接下来是根据需要,一步一步的介绍如果编写打包和调试脚本。
step-1 库的结构
在rootdir里,我们的workspaces里写的是匿名写法,workspaces: ["packages/*"]
,那么,packages下的文件名就是我们的库的名称,所以,文件名会十分重要。
文件名和库名保持一致,与packages/*/package.json
里的name保持一致,这样脚本在打包的时候才能一致的输入正确的 bundle文件,当然,也很推荐带 npm scope 的命名写法,防止发布包的时候冲突又要重新起名,代码两分钟,起名两小时(package.json 里的name 采用 @scope/lib-name这样的写法,如:@rollup/rollup-typescript)
为了合理有效的支持全量引入、esm tree-shaking ,src目录下有个index.js/ts 文件导入导出所有,并作为打包的入口文件,那么结构上就没有问题了。
step-2 通用的rollup.config.js
rollup和webpack都一样,保证input,确定output,中间plugin处理代码和资源。
拿到input
首先,我们的库都是放在packages目录下,需要传入打包的目标,就交给环境变量TARGET
来处理,于是:
import path from 'path'
const pkgsDir = path.resolve(__dirname, 'packages')
const pkgDir = path.resolve(pkgsDir, process.env.TARGET) // TARGET 我们将在脚本里提供
由于我们的rollup是由脚本启动,类似nodejs 执行 rollup.rollup() 那么,rollup在不知道bsaeUrl的时候,我们需要给rollup传入完整的文件路径,同时,可支持深层次的文件结构:
// 创建一个新的resolve 代替path原生resolve,前面已经处理好pkgDir
const resolve = (filename) => path.resolve(pkgDir, filename)
处理output
按照我们默认的情况,库的入口一般在src/index.ts,当然不排除一个库提供多个工具,如vite,同时提供build和cli脚手架功能,即,入口同时有src/index.ts, src/build.ts, src/cli.ts,那么,我们可以在package.json里用buildOptions来写入各自的差异。如此就可以通过覆盖参数的方式针对性的打包各个库:
// 针对构建,我们采用 cli inline > pkgOptions > defaults 的优先级 处理参数覆盖
const pkgOptions = pkg.buildOptions || {}
const defaultFormats = ['esm', 'umd']
// cli inline 表示你在命令行输入命令时,想手动控制打包行为而指定的命令,可以是打包格式,监听模式,或者dev环境等
const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',')
const packageFormats = inlineFormats || pkgOptions.formats || defaultFormats
没一个打包格式将对应一个输入输出,那么我们要根据format生成一个完整配置参数的数组,并导出给rollup使用:
const outputConfigs = {
esm: {
file: resolve(`dist/${name}.esm.js`),
format: 'es'
},
global: {
file: resolve(`dist/${name}.global.js`),
name: camelCase(name),
format: 'iife'
}
// .... 还有cjs umd 等
}
const packageConfigs = packageFormats.map(createConfigWithFormat)
function createConfigWithFormat(format) { return createConfig(format, outputConfigs[format])}
export default packageConfigs
对于umd,iife格式,rollup打包成立即执行函数的需要提供name属性用于挂载在global上,另外,生产环境需要更改output的文件名,以及压缩代码等,这部分差异显而易见且代码简单,就不在这里展示了。 这样,处理input和output就OK了:
//rollup.config.js 需要默认导出一个配置或配置数组,这个配置必须是一个输入对应一个输出
//由于plugins是通用的,我们就创建一个方法处理每一个不同的format
//format即是此次打包格式,output我输出配置,plugins是特殊需要的插件
function createConfig(format, output, plugins = []) {
//由于output在dev和production环境下输出并不一样,我们将差异化的地方放在外面
//使createConfig始终关注通用配置的默认行为
//同样,不同场景下可能会有一些特殊的插件要使用,那么除了同样的插件,这些特殊插件都通过传入的方式添加
const entryFile = pkgOptions.entry || 'src/index.ts'
return {
input: resolve(entryFile),
output,
plugins: [
common-plugin1(),
common-plugin2(),
...plugins
]
}
}
step-1 step-2 总结
- 我们需要知道打包目标(“project-1”)来自环境变量TARGET
- 我们需要知道打包格式(“esm, cjs, umd...”)来自cli inline, buildOptions,defaults
- 我们需要知道打包环境dev or productin 来自 env.NODE_ENV,或者知道更多的其他环境变量
step-3 开始编写打包脚本
dev打包
- cli inline 优先级最高,可以覆盖所有参数,所以我们要先获取命令行里的参数,这里推荐使用
minimist
库,自动解析并格式化参数
假设我们此次调试的库是shared
,打包格式是esm
, 命令行为:
yarn dev shared -f esm
# or
yarn dev shared --formats esm
# or
yarn dev --formats esm shared
输出的结果:
// 具名参数arguments 采用 --word 或 -w 方式(--接单词或-接单词缩写 接空格 接value的形式)
// 匿名参数则统一push到 args._ 里
const args = require('minimist')(process.argv.slice(2))
console.log(args) // target { _: [ 'shared' ], f: 'esm' }
- 如上我们已经可以随心定制命令行了,接下来就是定制rollup的命令行参数:
// -w 表示监听文件变化,-c表示使用config文件,默认rollup.config.js 也可以在value 指定
// --enviroment 可以传递环境变量 比如 NODE_ENV 这种
rollup -wc --enviroment [环境变量]
为了能执行命令行,我们还需要一个简化的命令行库execa
const execa = require('execa')
// 如果你正专注于某一个库的开发,还可以默认指定参数,或者在package.json scripts里指定打包对象
// fuzzyMatchTarget 用于检查当前target是不是在packages目录下存在
const target = args._.length ? fuzzyMatchTarget(args._)[0] : 'split-layout'
const formats = args.formats || args.f
const sourceMap = args.sourcemap || args.s
execa(
'rollup',
[
'-wc',
'--environment',
[
`TARGET:${target}`, // 传递打包对象
`FORMATS:${formats || 'global'}`, // 传递打包格式
// sourcemap 是代码打包后对于源代码的索引,方便浏览器报错时查找到错误代码位置
sourceMap ? 'SOURCE_MAP:true' : ''
].filter(Boolean).join(',')
],
{
stdio: 'inherit'
}
)
production打包
- 生产环境打包原理和上面一样,在主要流程上区别的地方主要是:
- 输出dts文件
- 同时打包多个库
- 生成参数类型文档
- gizp文件
- 去除开发环境提示
- 验证输出文件并check 文件大小
输出dts文件和类型文档
rollup-plugin-typescript2
打包ts代码并输出dts文件
需要用@rollup/plugin-typescirpt 或 rollup-plugin-typescript2
@rollup/plugin-typescirpt
是官方插件,rollup-plugin-typescript2
是基于官方插件新增了语法语义报错提示,所以我推荐后者,以便开发时就能提早发现问题。
既如此我们需要忘rollup的配置里加入 typescript插件
typescript插件会有默认配置,但是我们各个库可能有不同的目标,比如浏览器通常编译到es5啊,插件编译到es6啊,各种情况都有,那我们就在这个库下创建一个tsconfig.json 写上自己的需要的配置进行覆盖,同时也支持参数override,value是个对象为tsconfig里的配置,这里我们从简且需求达到,就如下了:
+ import typescript from 'rollup-plugin-typescript2'
function createConfig(format, output, plugins = []) {
return {
plugins: [
+ typescript({
+ tsconfig: resovle('tsconfig.json')
+ }),
...plugins
]
}
}
// packages/shared/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".", //重新指定参照路径
"declaration": true, // 是否输出声明文件
"declarationMap": true,
"outDir": "dist"
}
}
@microsoft/api-extractor
将dts文件整合到一起,并生成type doc
默认会使用配置文件api-extractor.json
,由于是通用的,这里就不展示了,可以直接去仓库里看
我们使用的是脚本文件,就需要手动调用api-extractor
插件了:
const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor')
const extractorConfigPath = path.resolve(pkgDir, 'api-extractor.json')
const extractorConfig = ExtractorConfig.loadFileAndPrepare(
extractorConfigPath
)
// invoke表示使用自定义准备好的文件,场景就是针对脚本执行时手动传入配置
const extractorResult = Extractor.invoke(extractorConfig, {
localBuild: true,
showVerboseMessages: true // 输出更为详细的信息
})
execa是异步执行函数,我们需要等待所有ts文件打包后输出对应的dts文件,才能将使用api-extractor
将dts文件整合到一起,如果本地还有types文件夹,build脚本里还有一段代码将types里的dts文件追加到dist下的dts文件里,详情请看仓库里的scripts/build.js文件
至此,打包上的重要步骤就完成了,别的插件可以按自己的需要添加。
gzip和输出文件大小信息
gzip就比较简单了,使用:
const { gzipSync } = require('zlib')
// 直接fs读取文件,gzip猛抽就完事
const file = fs.readFileSync(filePath)
const minSize = (file.length / 1024).toFixed(2) + 'kb'
const gzipped = gzipSync(file)
const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb'
总结一下操作
- 在根目录简单的命令行就可以单独调试某个库或者一键打包
yarn dev project-1 -f esm
yarn build -t
关键文件夹和文件:
tsconfig.json, scripts/dev.js, scripts/build.js, api-extractor.json, rollup.config.js
以上只是通用的packages/projects 目录格式的打包方式,你可以根据自己的需要自定义打包脚本,只要关注点 input, output, target, formats 是清晰的就没有问题
- 所有实例代码都在 仓库 compose/initial 下,这个分支只会优化bundle代码,不会写入业务代码
- 后期,这个仓库会参照vscode需求基于原生js写一个支持T字型,或者田字型布局的拖拽库,届时,欢迎捧场
这篇文章参考代码来自vue-next
,阅读完本文章之后,相信你有能力掌握vue-next项目的打包和构建流程
【github 仓库地址】
毕业工作两年了,开始一步步沉淀自己,新开的组织和github账号,不再瞎搞,一切重新开始,后面开始写文章了,少摸鱼,欢迎留言讨论和指出错误,同时欢迎大佬现场教学。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!