最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • monorepo 创建属于你的js/ts库

    正文概述 掘金(Octo1996)   2021-03-21   1172

    引言

    • 前端基建工程化"横行",专注于业务代码得不到提升,是不是觉得空虚觉得寂寞觉得冷?

    • 很多人想写个自己的库,但是没有方法,光有技术却得不到施展,总是在构建上遇到阻碍。

    • 每次构建的时候发现流行的库都用上了高级的构建技巧,自己还在用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 下存放的就是我们要开发的库,如上的project1project2

    就这么简单,当你在命令行敲下yarnyarn 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)

    monorepo 创建属于你的js/ts库

    monorepo 创建属于你的js/ts库

    为了合理有效的支持全量引入、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打包

    • 生产环境打包原理和上面一样,在主要流程上区别的地方主要是:
    1. 输出dts文件
    2. 同时打包多个库
    3. 生成参数类型文档
    4. gizp文件
    5. 去除开发环境提示
    6. 验证输出文件并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账号,不再瞎搞,一切重新开始,后面开始写文章了,少摸鱼,欢迎留言讨论和指出错误,同时欢迎大佬现场教学。


    起源地下载网 » monorepo 创建属于你的js/ts库

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元