之前自己其实接触过组件包,依赖包的项目搭建。追溯第一次发布 NPM 包那还是在搞 ReactNative 的时候开发和维护 ReactNative module,那时候初始化脚手架其实用的就是 create-react-native-module (https://github.com/brodybits/create-react-native-module) 工具生成的一个项目模版。直接用社区解决方案,开发和维护体验都被社区的开发者们调教的比较舒服了。

后面开发一个了 React Web 项目需要将部分共用组件和一些函数封装为一个 library 也有触摸到项目的依赖包开发调试的问题,为了图方便直接就是用 laravel-mix (https://laravel-mix.com/) 编译项目了(有一说一这玩意还挺香之前张哥在公司疯狂推这玩意的时候没感觉直到后来自己弄项目了)。通过简单的配置就能编译项目输出到指定文件,大部分 webpack 配置都帮你弄好了。但是,调试的时候确实比较麻烦,当时就遇到了我先改依赖包的文件然后编译之后再利用 yalc (https://github.com/wclr/yalc) 本地更新 NPM 依赖。每次改动依赖包都需要重新编译然后手动同步依赖,然后再重新编译项目才能使新的依赖代码调整生效,总之维护体验还是比较差的。

那么有没有一个优雅的依赖包调试方案呢?那答案是肯定,这篇博客我会尽量把自己理解的东西(关于前端工程化方面的)写上来,由于涉及的方面比较多可能会有一些混乱(慎看)。

首先我们先来总结一下需要实现的目标:

  1. 支持项目中 npm run build 直接构建子依赖包
  2. 支持监听子依赖包改变重新编译项目

这篇博客将重点讲通过编写 node 脚本支持项目中 npm run build 直接构建子依赖包,支持监听子依赖包改变重新编译项目主要是通过 vue-cli 配置 webpack-dev-server 实现。

我先描述一下接到的任务情况,目前是有一个项目引入了一个依赖 js 文件这边是有两种使用方式的,一种是直接有一个简单的 html 使用 script 标签对这个依赖文件进行了引入,另外一种就是这个项目本身是一个 vue2 的项目,在项目中将会以 EMS 方式导入使用。我的任务就是将这个依赖文件抽离为一个 npm 依赖包,当然少不了 typescript 于是我打算使用 vite 来构建。那么总结一下目前的情况就是需要支持 EMS 和 CJS 方式使用依赖包。(当然,新开坑的话可以都使用 vite 来构建统一下构建工具,这样反而会更舒服,且不限于是 Vue 还是 React 项目)

首先使用 vue-cli 创建一个项目,在项目中创建 packages 文件夹用于放我们的依赖包(这里面可以放多个相关的依赖包互不影响,我目前就只用一个包做演示)在 packages 文件夹中用 vite 创建依赖包文件夹。

如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝

我在上面创建了一个 lib-welcome 依赖,创建 index.ts 写点代码,然后需要配置一下 vite.config.ts 使其编译 CJS 产物。

如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝
import { defineConfig } from 'vite'
import { resolve } from 'node:path'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [],
    build: {
        lib: {
            // Could also be a dictionary or array of multiple entry points
            entry: resolve(__dirname, './index.ts'),
            name: 'LibWelcome',
            // the proper extensions will be added
            fileName: 'lib-welcome',
        },
    }
})

这里配置的 name 就是在 CJS 中会注册到全局的对象名称,通过 script 标签引入后使用这里配置的 name 就可以调用函数(具体代码看下文实现 CJS 调用页面)。

如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝

在 lib-welcome 中执行 npm run build 编译后就能看到,构建产出了 lib-welcome.js 和 lib-welcome.umd.cjs 两个文件。现在编辑 package.json 添加 main 并指定需要发布的文件就可以 publish 到 NPM 上了。

如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝

如果你的依赖包代码较多的话建议创建 src 并且将其添加到 files 中,当然,如果你不想将源代码上传的话可以将 main 字段指定为 ./dist/lib-welcome.js 并在 files 中删除源代码部分。

现在我们需要执行 npm install ./packages/lib-welcome/ 在 vue-library-tmp 项目中引入 lib-welcome 并使用。

修改一下 App.vue 的代码,导入 @zmide/lib-welcome 依赖,调用 getWelcomeMessage 函数试试

如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝

好了,到这里一个简单的依赖包就准备好了。然后就是要支持 CJS 方式使用了。

首先将依赖中编译好的 welcome.umd.cjs 文件拷贝过来,然后再在项目的 public 中创建一个用于测试的 debug.html 页面

如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝
如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝

在 debug.html 页面中就利用 LibWelcome 调用函数,是没有问题的。

虽然现在来说项目已经能跑了,但是每次我们改变 lib-welcome 依赖包中的代码首先需要编译一下 lib-welcome 依赖包,然后再重新编译主项目,这调试过程确实有点难受了。

于是引出,我们要怎么优化这个编译的流程,正如我们在目标里说的,要实现在主项目直接 npm run build 就能够直接帮我们编译好 lib-welcome 依赖包,随便帮我拷贝好 welcome.umd.cjs 文件到 public 文件夹下就好了。

我最开始是准备尝试通过配置 vue.config.js 或者 webpack 来实现这个过程的。实际上目前大部分项目都采用的是自定义一个 node 脚本来编译项目,其实主要还是这个脚本的流程思路。

遵照国际标准(不是),在项目主目录创建一个 scripts 文件夹,并且添加一个 build.js 文件,然后改一下我们主项目的 package.json 中 build 命令,让它使用这个 build.js 来构建项目。

如何优雅的提供一个前端现代化依赖包?通过编写 node 脚本扩展编译依赖包。-天真的小窝
{
  "name": "vue-library-tmp",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "node scripts/build.js",
    "lint": "vue-cli-service lint"
  },
 ……
}

这个 build.js 脚本怎么写呢?其实就是判断 lib-welcome 是否已经编译好了,如果编译好了就通过构建产出物的 md5 判断主项目中的是否需要更新,如果要更新的话就复制新编译的文件到主项目。

在写代码前需要先安装 shelljs (https://github.com/shelljs/shelljs),利用 shelljs 执行 shell 命令来构建或者复制文件。

npm install -D shelljs

const fs = require("fs");
const crypto = require('crypto');
const { resolve } = require('path');
const shell = require("shelljs");

const libraryDirName = "lib-welcome" // 依赖文件夹名
const productName = "lib-welcome.umd.cjs"; // 构建产物文件名

const rootDir = resolve(__dirname, '../');
const publicDir = resolve(__dirname, '../public');
const libraryDir = resolve(__dirname, `../packages/${libraryDirName}/`);
const libraryFileCjs = resolve(__dirname, `../packages/${libraryDirName}/dist/${productName}`);
const publicCjs = resolve(__dirname, `../public/${productName}`);

function getFileHash256Sync(file) {
    const buffer = fs.readFileSync(file);
    const fsHash = crypto.createHash('sha256');
    fsHash.update(buffer);
    const md5 = fsHash.digest('hex');
    return md5;
}

try {
    fs.accessSync(libraryDir)
    console.log("build library");

    // 构建 lzcAppExt
    shell.cd(libraryDir);
    shell.exec("npm ci && npm run build", { async: true }, async function (code, data, error) {
        if (code !== 0) {
            console.log("library build error 001");
            return;
        }

        let distFileMD5 = "";
        if (fs.existsSync(libraryFileCjs)) {
            distFileMD5 = getFileHash256Sync(libraryFileCjs);
        }

        let oldDistFileMD5 = ""
        if (fs.existsSync(publicCjs)) {
            oldDistFileMD5 = getFileHash256Sync(publicCjs);
        }

        if (distFileMD5 === oldDistFileMD5) {
            // 构建输出没有更新,不需要拷贝
            return;
        }

        shell.cp(libraryFileCjs, publicDir);
    });
} catch (error) {
    console.log("依赖包不存在跳过构建");
}


// 构建项目
shell.cd(rootDir);
shell.exec("npx vue-cli-service build", { async: true });

现在执行 npm run build 构建项目应该是没问题了。

最后就是配置主项目 vue.config.js 中 webpack-dev-server 的 watchFiles 项,让其也监听 packages 下的代码改变就好了。

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    watchFiles: ['src/**/*', 'packages/**/*']
},
})

最后,修改 lib-welcome 依赖中的代码时,项目就会实时编译了。

项目代码我都发布到 https://github.com/PBK-B/vue-library-tmp 仓库了。