前言

你曾经关注的失踪人口名单博主更新啦,按照国际惯例(bushi,先让我讲几句废话。不喜欢听废话的小伙伴可以直接略过哈…

在写了几个小功能的依赖包之后,越发发现曾经自己在前端工程上面的无知,真是一入前端深似海。我甚至到最近才知道 monorepo (单仓库) 和 multirepo (多仓库) ,虽然我已经用过很多或者说公司的很多项目都涉及到这两种软件开发策略,我只是深处其中并不知其意。之前在弄的一个依赖包创建一个 packages 文件夹也只是在 React 的仓库中看到这种方式且觉得这样拆分多个包的技术很有意思,然后就尝试这样放依赖包。这两天无意中看到这个 multirepo 这个词,于是才了解到原来这里面也是有些东西的。说回依赖包,我之前写了一篇《如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。》我现在回头看,感觉远远不够优雅,甚至里面有很多逻辑非常混乱。

当然,那只是一个开始,我自认为是我开始了解前端工程化的起点,才踏进门的我觉得这很有意思,我大概在一年前就一直喊着要学习前端工程化,但是实际上一直没摸到门槛,一直都在搭建一个网站的脚手架上就是 admin 管理后台模版那种水准。当然很幸运的是当时腾讯开源的 tdesign-react UI 框架刚开始开源,然后我也是头铁,直接就上到一个线上项目中了,于是乎不出意外的遇到一些 BUG,然后就尝试去反馈提了 issue (我其实已经知道问题且直接改 node_modules 解决了),腾讯的大佬就说让我提交一个 PR,于是乎我就开始了参与 tdesign-react 项目的贡献(虽然我是一个水货,总共才提交了几个 PR)但是居然混到一个活动,拿到了一些小奖品。也学到很多东西,其实我现在搭建的一个依赖包就参考了 https://github.com/Tencent/tdesign-react 其中 rollup 的配置也很大程度的参考了 tdesign-react 配置结合文档写的。其实最大的收获还不止如此,在此期间我学会了怎么写测试用例,如何参与开源项目等等…

好像有点水过头了,最后我再说说项目背景吧,我在公司负责一个 Hybrid App 客户端的开发就有原生和 JS 进行交互和提供相应原生功能的部分(不要问我为啥不用 React Native 因为不只是我一个人写,要是我自己一个人写那可能就是 RN 一把梭了,团队技术选型还是尊重团队的技术背景所以我们 Web 就直接用 Vue 了)刚开始的时候,其实就是整了一个 js 文件,然后我们不是又很多页面(项目)嘛(可以理解为有很多小程序),他们使用这个依赖包的方式就是通过手动拷贝(对,你没听错就是手动拷贝)刚开始的时候也是比较简单就几个函数也还能理解,当我抵达战场的时候已经是这个项目的 js 长这样,那个项目的 js 长那样,我都不知道该遵照谁实现这个函数了…

然后我提出了统一“大汉”的想法,于是乎我就开始整理发布了第一个 ext 依赖包,虽然大家都认可我这个 ext 包,但是在适配上还是遇到了一些小问题(主要是大家手上都还有其他活),于是我主动参与了几个项目的适配推进工作。于是第一阶段的前端依赖包“工程化”就这么开始了,《如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。》 这篇博客就是在那个时候写的。

不过没多久,夏老大说之后客户端里应该要提供一个 sdk 给第三方开发者(可以理解为微信小程序那种 js sdk 用于第三方开发者开发小程序应用运行在我们客户端中),需要提供一个跨端统一的 SDK(我们这里的跨端不仅要适配 Android 和 IOS 且要兼容浏览器中有正常行为)于是我开始了第二阶段的“工程化建设”,由于 ext 的经验和上面遇到的一些问题,我在 sdk 里使用了统一的对象管理 ios / android 原生注入的对象,并且通过提供唯一调用方式拉平平台实现差异,还有通过注解的方式来拦截和标注当前函数支持的平台避免不支持的平台调用函数报错导致页面异常(拦截执行后只在控制台弹出警告信息)。在这个过程中还遇到宋师傅(Android 开发)那边由于对前端代码不熟悉提交了一个错误的代码,然后提交了代码并编译发布到 npm 上导致全部项目都歇菜的小故事,于是乎我就整上了 eslint 在编译时会先跑一遍 eslint 测试代码规范性(我已经意识到团队合作时一个良好的软件工程是挺有用的)。当然,我也是有配置了一下 prettier 但是并没有加到 eslint 检查规则中(有时候还是能遇到一些小伙伴没有格式化代码带着千奇百怪的缩进就提交上来了,在下个阶段的依赖包建设中我就强制加到 eslint 中了而且我还配置了 commit 提交前进行 eslint 检查不合规的代码可能提交都提交不了 ? ,这是也是和 tdesign-react 学的,我还一直记得我第一次提交代码的时候提交不了的一脸懵逼,当然这是后话)

终于到了第三个阶段,我在整完第二阶段的 sdk 的时候就想将 ext 也重构成 sdk 这种架构的,在 ext 日益壮大的时候(宋师傅和前端那边用 ext 用的越来越熟练,原生和客户端中页面的功能交互越来越多),里面的函数也从当初的几个增长到十几个,之前 ext 其实是将 android 和 ios 的实现都杂乱的堆在一起的,夏老大说客户端之后的 UI 直接分开,其实很多很多函数都只需要支持其中一个平台就好了,需要将 ext 重构为新的 jsbridge ,然后我就开始了新的折腾之路从 tsc 、rollup、eslint、prettier 到 husky(当然还有我给泽霖画的大饼,之后要增加测试用例和支持能够直接一键自动化测试 jsbridge 函数的可用性)。

认识 rollup

其实应该用不着我来介绍 rollup 的,应该看到我这篇博客的小伙伴们应该都知道 rollup 是啥。但是还是让我稍微从官网抄几句英语装个 B 吧(手动狗头。

Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.(Rollup 是一个 JavaScript 模块打包工具,可以将多个小的代码片段编译为完整的库和应用。)

如果你是一个前端应该或多或少应该用过或者听说过 webpack 吧,Rollup 和 webpack 一样是前端打包工具的一个分支。在体验过 Rollup 的配置之后,我真的觉得 webpack 的配置真的难搞 ?

接下来我们先创建一个项目文件夹,就叫 universal-library-ts

# 创建文件夹
mkdir universal-library-ts
cd universal-library-ts

# 初始化项目
npm init

紧接着当然是安装 typescript 和添加 src/index.ts 代码文件

yarn add --dev typescript

# file src/index.ts
/**
 * 打招呼函数
 * @param {string} name
 * @return {void}
 */
function hello(name: string): void {
    console.log("hello,", name);
}

export { hello }

我们先把 tsconfig.json 生成一下,当然可以参考我代码仓库的 tsconfig 配置进行修改(tsc 的配置我有空可以单独写篇博客我觉得,如果我不鸽的情况下 ?️),rollup 也是使用 @rollup/plugin-typescript 插件进行编译的所以配置文件我就直接使用已经配置好的 tsconfig 了,由于这是讲 rollup 和其一些插件的博客,这里就不深入解释 ts 的配置文件了。

{
    "compilerOptions": {
        "target": "ESNext",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "moduleResolution": "Node",
        "strict": true,
        "jsx": "preserve",
        "importHelpers": true,
        "resolveJsonModule": true,
        "isolatedModules": false,
        "esModuleInterop": true,
        "declaration": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "allowSyntheticDefaultImports": true,
        "lib": ["ESNext", "DOM"],
        "skipLibCheck": true,
        "noEmit": false,
        "allowJs": true,
        "checkJs": true,
        "downlevelIteration": true,
        "outDir": "dist",
        "rootDir": "src",
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        }
    },
    "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
    "exclude": ["dist", "node_modules"],
    "compileOnSave": true
}

开始配置 rollup

好了,终于开始安装 rollup 和 @rollup/plugin-typescript 等一些需要用到的插件了

# 安装 rollup
yarn add --dev rollup

# 安装 typescript 编译插件
yarn add --dev @rollup/plugin-typescript tslib

# 安装 @rollup/plugin-node-resolve 插件
yarn add --dev @rollup/plugin-node-resolve

# 安装 @rollup/plugin-commonjs 
yarn add --dev @rollup/plugin-commonjs 

创建 rollup.config.js 配置文件,首先定义一些参数

import path from 'node:path'
import glob from 'glob'
import { URL, fileURLToPath } from 'node:url'
import process from 'node:process'

const name = 'UniversalLibrary'
const fileName = 'index'
const umdInput = 'src/index.ts'
const outputDir = 'dist/'
let sourcemap = false
let isProd = true
const inputList = Object.fromEntries(
    glob.sync('src/**/*.ts').map(file => [path.relative('src', file.slice(0, file.length - path.extname(file).length)), fileURLToPath(new URL(file, import.meta.url))])
)

if (process.env.ENV === 'development') {
    // 开发模式时
    isProd = false
    sourcemap = true
}

这里导入了一些 node 的模块,rollup 的执行环境也是 node 所以可以在 rollup.config.js 直接写逻辑。name 定义导出的对象名,fileName 定义导出文件名,umdInput 定义通用模块的文件,outputDir 定义输出文件夹,sourcemap 定义是否输出源代码映射文件,isProd 定义是否为生产环境,inputList 从 src 文件夹下读取全部的 ts 文件的数组

然后写了一个 if 判断有没有传 ENV 参数为 development ,如果传了的话就是开发模式同时修改 sourcemap 和 isProd 变量

接着,我们定义一个 getPlugins 函数,将要使用的插件都在这个函数中配置好,然后返回一个插件数组,当然如果需要支持 tsx 的编译(比如需要写一个 UI 组件库)还得安装 babel 相关的插件,我这里由于只是一个简单函数就不安装了

import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'

const getPlugins = ({ isProd = true } = {}) => {
    const plugins = [
        nodeResolve(),
        commonjs(),
        typescript({ tsconfig: './tsconfig.json' }),
    ]
    return plugins
}

最后直接构建几个配置对象,我这里暂时只配置 ES module 和 Universal Module 的打包。

// ES module file
const esmConfig = {
    input: inputList,
    treeshake: false,
    plugins: getPlugins({
        isProd,
    }),
    output: {
        format: 'esm',
        sourcemap,
        chunkFileNames: '_chunks/dep-[hash].js',
        dir: outputDir,
    },
}

// Universal Module
const umdConfig = {
    input: umdInput,
    plugins: getPlugins({
        isProd,
    }),
    output: {
        name,
        format: 'umd',
        exports: 'named',
        sourcemap,
        file: `${outputDir}${fileName}.umd.js`,
    },
}

const umdMinConfig = {
    input: umdInput,
    plugins: getPlugins({
        isProd: true,
    }),
    output: {
        name,
        format: 'umd',
        exports: 'named',
        sourcemap,
        file: `${outputDir}${fileName}.umd.min.js`,
    },
}

export default [esmConfig, umdConfig, umdMinConfig]

是的,这就配置好了现在只需要将编译脚本写到 package.json 中就大功告成了。

{
  "name": "universal-library-ts",
  "version": "1.0.0",
  "description": "这是一个通用的前端依赖包脚手架",
  "main": "dist/index.js",
  "module": "dist/index.js",
  "typings": "dist/index.d.ts",
  "unpkg": "dist/index.umd.js",
  "jsdelivr": "dist/index.umd.min.js",
  "type": "module",
  "scripts": {
    "dev": "npx rollup -c ./rollup.config.js --environment ENV:development",
    "build": "npx rollup -c ./rollup.config.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^24.0.1",
    "@rollup/plugin-node-resolve": "^15.0.1",
    "@rollup/plugin-typescript": "^11.0.0",
    "rollup": "^3.20.2",
    "tslib": "^2.5.0",
    "typescript": "^5.0.3"
  }
}

最后运行 yarn build 应该能得到如下的编译结果。

【前端脚手架搭建】开发前端依赖包系列之如何配置 rollup 构建一个友好的前端依赖包-天真的小窝

代码仓库地址: https://github.com/PBK-B/universal-library-ts

好了,今天就先写到这里吧,希望后续的博客我不会鸽 ?️