首页 > 其他分享 >从零到一构建并打包 React + TypeScript + Less组件库教程(二、组件库编译多产物及文档编写)

从零到一构建并打包 React + TypeScript + Less组件库教程(二、组件库编译多产物及文档编写)

时间:2024-11-14 11:44:00浏览次数:1  
标签:gulp TypeScript less babel js React webpack 组件 css

本系列目录如下:

  1. 项目初始化搭建+代码规范集成
  2. 组件库多产物编译及文档编写

上篇文章我们将组件库的基本结构和规范进行了整理,本篇的核心基本全在 components 文件夹下

本篇的打包参考了文章 https://github.com/worldzhao/blog/issues/5 ,强烈建议阅读一下此文章,而且讨论区也能让你收获良多。

本章节分成了两次 commit

安装组件开发依赖

react 和 react-dom 依赖集成

  • package.json 文件的 peerDependencies 写入 react react-dom
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },

执行 pnpm install

为什么是 peerDependencies 而不是 devDependencies

peerDependencies 可以约束使用该包的宿主环境,控制其兼容依赖的版本在指定范围内,而 devDependencies 则是纯粹的开发依赖

  • 安装 reactreact-dom 的 types 类型包
pnpm i @types/react @types/react-dom -D

clsx

安装 clsx 作为样式开发的库,其它用到了再安装即可

pnpm i clsx -D

编写 less 样式

  • componets/src 下新建 style 文件夹
  • 新建 index.less 作为入口文件
  • 新建 normalize.less 重置样式,直接去 arco design 仓库 拷贝下来即可。
  • style 文件夹下新建 themes 文件夹,在 themes 下新建 default.less,声明一些默认 less 变量
// packages/components/src/style/themes/default.less

@prefix: rclt;

@font-family:
  Inter,
  -apple-system,
  BlinkMacSystemFont,
  PingFang SC,
  Hiragino Sans GB,
  noto sans,
  Microsoft YaHei,
  Helvetica Neue,
  Helvetica,
  Arial,
  sans-serif;

@font-size-body: 14px;

@line-height-base: 1.5715;
  • index.less 中引入 default.lessnormalize.less
@import './themes/default.less';
@import './normalize.less';

此时的文件结构应该是这样的

- packages
  - components
    - src
      - style
        - themes
          - default.less
        - index.less
        - normalize.less

写这么多是为了在后续编译 less 样式时能看出明显的效果。

编写测试组件 Button

代码我就不列出来了,大家可以自行编写,或者去仓库看也可以,就是一个简单的示例组件,编写完成后,目录结构如下

image

  • 由于样式后续要做按需导入,所以必须确保每个组件的样式 style 有一个导出的 index.(ts|js) 文件和 index.less 文件
  • index.(ts|js) 的内容就是 @import xxx.less

src/index.ts 中统一导出组件

export type { ButtonProps } from './button/interface';
export { default as Button } from './button';

打包编译

  • 导出类型声明文件
  • 导出 umd esmodule commonjs 3 种形式产物供使用者引入;
  • 支持样式文件 css 引入,而非只有 less,减少使用者的接入成本;
  • 支持按需加载。

导出类型声明文件

tsconfig.json 上一节已经写好了,直接使用 tsc 编译即可。

执行命令之前需按照 cpr,执行 pnpm i cpr -D

  "scripts": {
    "build:types": "tsc -p tsconfig.json --outDir es  && cpr es lib"
  },

这个命令就是单纯用 tsc 编译出了类型 d.ts 文件导出到 es 文件夹里,cpr es lib 就是将 es 中的 d.ts 文件拷贝到了 lib 目录下,此时执行 pnpm build:types 即可看到生成这两个文件夹

其实大部分 npm 包需要做的都是编译,打包基本用不到。

导出 commonjs 代码

打包这个工作,rollup 也可以,不过我们是编译,不是打成 bundler,而且还要处理 css,rollup 就不是很容易处理了。如果是直接打 bundler 的话,rollup 更简单,而且 webpack 在这方面也很在行。

babel 相关配置

安装 babel 及其相关依赖,这一部分解释性的话术引用自开头推荐的文章内容。

pnpm i @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime -D
pnpm i @babel/runtime-corejs3

新建 .babelrc.js,写以下内容

module.exports = {
  presets: ['@babel/env', '@babel/typescript', '@babel/react'],
  // @babel/plugin-transform-runtime 的 helper 选项默认为 true
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,
        helpers: true,
      },
    ],
  ],
};

关于 @babel/plugin-transform-runtime@babel/runtime-corejs3

  • 若 helpers 选项设置为 true,可抽离代码编译过程重复生成的 helper 函数(classCallCheck, extends 等),减小生成的代码体积;
  • 若 corejs 设置为 3,可引入不污染全局的按需 polyfill,常用于类库编写(更推荐:不引用 polyfill,转而告知使用者需要引入何种 polyfill,避免重复引入或产生冲突,后面会详细提到)。
  • 更多参见官方文档 @babel/plugin-transform-runtime

为了避免转译浏览器原生支持的语法,新建 .browserslistrc 文件,根据适配需求,写入支持浏览器范围,作用于 @babel/preset-env。(该文件最终只会作用于 css,后续会有相关解释)

> 1%
last 2 versions
Firefox ESR
not dead
IE 11
not IE 10

polyfill 相关思考

很遗憾的是,@babel/runtime-corejs3 无法在按需引入的基础上根据目标浏览器支持程度再次减少 polyfill 的引入。

这意味着 @babel/runtime-corejs3 甚至会在针对现代引擎的情况下注入所有可能的 polyfill:不必要地增加了最终捆绑包的大小。

对于组件库(代码量可能很大),建议将 polyfill 的选择权交还给使用者,在宿主环境进行 polyfill。若使用者具有兼容性要求,自然会使用 @babel/preset-env + core-js + .browserslistrc进行全局 polyfill,这套组合拳引入了最低目标浏览器不支持 API 的全部 polyfill。

所以组件库不用画蛇添足,引入多余的 polyfill,写好文档说明,比什么都重要。

现在 @babel/runtime-corejs3 更换为 @babel/runtime,只进行 helper 函数抽离。

pnpm uni @babel/runtime-corejs3

pnpm i @babel/runtime

.babelrc.js

module.exports = {
  presets: ['@babel/env', '@babel/typescript', '@babel/react'],
  // @babel/transform-runtime 的 helper 选项默认为 true
  plugins: ['@babel/plugin-transform-runtime'],
};

gulp 任务编排

安装 gulp 相关依赖

pnpm i gulp gulp-babel @types/gulp @types/gulp-babel -D

注意: gulp-babel 目前存在问题,当 .browserslistrc 内不支持 IE 时会报错,详情可见 issue

新建 gulpfile.js,写入以下内容

const gulp = require('gulp');
const babel = require('gulp-babel');

const paths = {
  dest: {
    lib: 'lib',
    esm: 'es',
    dist: 'dist',
  },
  compileStyles: 'src/**/index.less', // 编译样式的入口文件,后续会解释为什么样式处理分成两部分
  copyStyles: 'src/**/*.less', // 样式文件路径
  scripts: ['src/**/*.{ts,tsx,js,jsx}'], // 脚本文件路径
};

function compileCJS() {
  const { dest, scripts } = paths;
  return gulp
    .src(scripts)
    .pipe(babel())
    .pipe(gulp.dest(dest.lib));
}

// 并行任务 后续加入样式处理 可以并行处理
const build = gulp.parallel(compileCJS);

exports.build = build;

exports.default = build;

有 eslint 报错,不允许 require 导入,去 eslint.config.mjs 关闭一下

    rules: {
      // ...
      '@typescript-eslint/no-require-imports': 'off',
    },

package.json 添加脚本命令 clean:build build

  "scripts": {
    "build:types": "tsc -p tsconfig.json --outDir es  && cpr es lib",
    "clean:build": "rimraf lib es dist",
    "build": "npm run clean:build && npm run build:types && gulp"
  },

由于 rimraf 基本子包都要使用,就安装的根目录的依赖下全局共享

pnpm i rimraf -D -w

然后执行命令 pnpm build,就能在 lib 目录下看到 commonjs 的代码了,且诸多 helper 方法已被抽离至 @babel/runtime

image

导出 ESM 代码

修改 babel 配置

为了让 ES Module 更好的支持 Tree Shaking,需要对 babel 配置做一些改动

  • 关闭 @babel/preset-env 对模块语法的转换,即设置 modules 为 false
  • 但是又需要只针对 esm,所以就根据环境变量做一个区分,esm 环境下才关闭(当任务执行时,设置当前的执行环境即可)
module.exports = {
  presets: ['@babel/env', '@babel/typescript', '@babel/react'],
  // @babel/plugin-transform-runtime 的 helper 选项默认为 true
  plugins: ['@babel/plugin-transform-runtime'],

  env: {
    esm: {
      presets: [['@babel/env', { modules: false }]],
    },
  },
};

gulp 新增 esm 构建任务

esm 和 cjs 都走 babel 编译,流程基本一致,可以将编译方法抽离出去,两个任务共用

// ...

/**
 * 编译脚本文件
 * @param {("esm"|"cjs")} babelEnv babel环境变量
 * @param {String} destDir 目标目录
 */
function compileScripts(babelEnv, destDir) {
  const { scripts } = paths;
  process.env.BABEL_ENV = babelEnv;

  return gulp
    .src(scripts)
    .pipe(babel())
    .pipe(gulp.dest(destDir));
}

/**
 * 编译cjs
 */
function compileCJS() {
  const { dest } = paths;
  return compileScripts('cjs', dest.lib);
}

/**
 * 编译esm
 */
function compileESM() {
  const { dest } = paths;
  return compileScripts('esm', dest.esm);
}

// 串行执行编译脚本任务(cjs,esm) 避免环境变量影响 gulp.series(compileCJS, compileESM)
// 并行任务 后续加入样式处理 可以并行处理  gulp.parallel(...)
const build = gulp.parallel(gulp.series(compileCJS, compileESM));

// ...

执行 pnpm build,观察 es 目录下的编译结果,都是 import 的 esm 写法

image

Less 样式处理

  • 了解流行组件库的样式为什么会是那样的结构
  • 模仿流行组件库的样式结构进行编译,实现样式的按需加载

为什么是 less?而非 sass 或 css in js?

  • 个人更对 less 熟悉一些,所以是 less 而不是 sass
  • 对于 cssinjs,个人不喜欢
    • 好处就是定制化极为方便,而且由于是 js,完全不用在编译时处理样式,自动带有 tree shaking,文章后续对样式的编译处理什么的统统没有
    • 被人诟病的就是性能,虽说抛开剂量谈毒量都是扯淡,但真拿剂量说事儿时一问一个不吱声。
    • 此处就稍微吐槽一下 antd5,cssinjs 性能做得真的很有问题,组件文档打开速度相对于 v4 来说不知道慢了多少倍(加载时大量的 style 标签动态插入),issue 里也经常有人反映性能问题。
    • 虽说有些零运行时的 cssinjs,但是那些随机类名个人看着也确实不舒服。

了解流行组件库的样式打包构成

这里就举例国内采用同样技术的组件库:antd 4.xarco designtdesign ,可以参考一下他们的打包后的结构目录

image

可以看到以上组件库的样式结构基本都一样(tdesign 直接使用 cssvar,但是对外提供了 less 能力),提供了 index.js(内部是 less 文件的导入)、css.js(内部是 css 文件的导入)index.css(内部是合并后的纯 css)以及原样的 less 文件

为什么要做的这么麻烦,给用户提供这么多种格式的 css/less 样式文件?组件库底层抹平差异,改善用户体验。

  • 提供 less 文件是为了给使用 less 的用户提供主题定制的能力(变量覆盖)
  • 提供 css 是为了兼容非 less 用户的使用,可以直接导入 css 而无需额外装 less-loader,属于 dx 优化。
  • 灵活的样式类型拆分可以给开发者更多的选择,选择 less 还是 css 进行开发都是可以的。
  • 拆分成多种类型的入口文件,还可以让用户做按需导入
  • 由于要灵活配置,所以开发的组件内部是不能直接导入样式的(不然的话就是写死样式,就不存在多个样式类型的引入方式),样式导入交给用户去做

拷贝 less 文件至打包目录

将上述的 less 打包结构理解之后,就可以按照这种结构开始打包样式了。

将开发中使用的 less 文件拷贝至 npm 包中,用户使用时,就可以按需引入 less 文件,也可以做 less 变量的覆盖。

gulpfile.js 中新建 copyLess 任务

/**
 * 拷贝less文件
 */
function copyLess() {
  return gulp.src(paths.copyStyles)
             .pipe(gulp.dest(paths.dest.lib))
             .pipe(gulp.dest(paths.dest.esm));
}

// gulp.parallel 的 args 是同时执行,gulp.series 的 args 是一个执行完毕执行下一个
const build = gulp.parallel(gulp.series(compileCJS, compileESM), copyLess);

可以看到 less 样式已经按照原来的结构 copy 到 es 和 lib 包中,然后就是生成 css 的步骤。

image

less 编译成 css

安装相关依赖,gulp-less 将 less 编译成 css,gulp-autoprefixer 添加 css 前缀,由于之前设置了 .browserslistrcgulp-autoprefixer 会自动识别兼容的版本去添加前缀

pnpm i gulp-less gulp-autoprefixer@^8 @types/gulp-less @types/gulp-autoprefixer -D

必须安装 gulp-autoprefixe 8.x,9.x 只支持 esm 导入。

gulpfile.js 中新增编译方法

/**
 * 生成css文件
 */
function less2css() {
  return gulp
    .src(paths.compileStyles)
    .pipe(less()) // 编译 less 文件
    .pipe(autoprefixer()) // 根据 browserslistrc 增加前缀
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.esm));
}

const build = gulp.parallel(gulp.series(compileCJS, compileESM), copyLess, less2css);

执行 pnpm build,检查打包文件,如图所示就是成功的。

image

这里没有对 css 进行压缩,esm 和 lib 会被用户以 npm 方式使用,用户打包时,自然会对 css 进行压缩。

为什么只对 index.less 进行编译?

  • 每个组件的样式都需要一个合并起来的入口文件,这个文件里引入了该组件所需的所有 less 样式,方便开发者导入。
  • 如果每个 less 都进行编译,那 index.less 编译出来的就是该组件所有的 css,然后其余的拆分组件也会再编译对应的 css,相当于重复编译了
    • index.less 编译后具有全量的该组件 css
    • index.less 引用了 a.lessa.less 再次被编译成 css,这个是没有意义的,反而造成了重复编译
    • 最大的问题是,如果所有 less 都进行编译,在编译完 index.less 后,会去单独编译其它的 less 文件,如果这些文件内使用了的 less 变量是通过 index.less 间接引入的,而 gulp-less 将其视为独立文件,就会产生 less 变量未定义的错误。

如果以上文字解释看不太明白,可以看如下举例:

// var.less
@font-size: 14px

// component.less
.cp{
  font-size: @font-size;
}

// index.less
@import "./var.less";
@import "./component.less";

这种情况下,component.less 使用了 var.less 的变量,但文件内没有直接导入 var.less,而是使用 index.less 做了间接使用,所以 component.les 是不具备独立编译的能力的。

如果全部 less 文件都做编译

  • gulp-less 在编译 index.less 时,由于有引入顺序,相当于把 var.lesscomponent.less 做了合并,这种是完全没问题的,可以正常编译。
  • gulp-less 编译完 index.less,再去编译 component.less,发现内部有一个未知的 less 变量,因为此时 gulp-less 不通过 index.less 这个桥梁走,而是直接编译 component.less,那自然会报错。

生成 css.js

生成 css.js 让不安装 less 插件的用户也可以正常使用。

功能实现参考 antd-tools,由于 antd5 现在不使用 less 了,就直接找到之前的 commit 把代码贴出来了,如下图所示

image

这段代码做的就是匹配到 style/index.js 时,生成 style/css.js,并通过正则将文件内容中引入的 less 文件后缀改成 css。

  • 安装 through2
pnpm i through2 -D

compileScripts 进行补充

/**
 * 编译脚本文件
 * @param {("esm"|"cjs")} babelEnv babel环境变量
 * @param {String} destDir 目标目录
 */
function compileScripts(babelEnv, destDir) {
  const { scripts } = paths;
  process.env.BABEL_ENV = babelEnv;

  return gulp
    .src(scripts)
    .pipe(babel())
    .pipe(
      through2.obj(function (file, encoding, next) {
        this.push(file.clone());

        if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
          const content = file.contents.toString(encoding);
          file.contents = Buffer.from(cssInjection(content)); // 文件内容处理
          file.path = file.path.replace(/index\.js/, 'css.js'); // 文件重命名
          this.push(file); // 新增该文件
          next();
        } else {
          next();
        }
      }),
    )
    .pipe(gulp.dest(destDir));
}

其中的 cssInjection 实现,还是在 gulpfile.js

/**
 * 当前组件样式 import './index.less' => import './index.css'
 * 依赖的其他组件样式 import '../test-comp/style' => import '../test-comp/style/css.js'
 * 依赖的其他组件样式 import '../test-comp/style/index.js' => import '../test-comp/style/css.js'
 * @param {String} content
 */
function cssInjection(content) {
  return content
    .replace(/\/style\/?'/g, "/style/css'")
    .replace(/\/style\/?"/g, '/style/css"')
    .replace(/\.less/g, '.css');
}

执行 pnpm build,即可看到 css.js 文件

image

其实这一部分很多可以优化的地方,做的更细致一点,大家在看懂之后可以自行尝试优化,比如 token.css 并不存在(token.less 未被编译),可以去掉,再比如可以把 less 变量编译 cssvar,css.js 可以再额外引入 css 变量,就可以做到动态换肤功能。

按需加载

实际上只要结构上写出来,按需加载的核心就已经完成了。

package.json 中增加 sideEffects 属性,配合 ES module 达到 tree shaking 效果(将样式依赖文件标注为side effects,避免被误删除)。

cssinjs 的库都不需要这个,因为 cssinjs 只有 js,天然支持 tree shaking

  "sideEffects": [
    "dist/*",
    "es/**/style/*",
    "lib/**/style/*",
    "**/*.less"
  ],

好,此时按需加载的步骤就已经完成了,大家如果用 webpack,可以借助 babel-plugin-import 实现按需导入样式,如果是 vite,可以使用 vite-plugin-imp

以上内容作为一次 commit 暂存一下:https://github.com/json-q/rc-library-templete/commit/c9d8a53c26d1189cd05939f57d07b07bfd2c1642

整理 package.json 的入口以及导出模块的方式和指向

{
  "main": "lib/index.js",
  "module": "es/index.js",
  "types": "es/index.d.ts",

  "files": [
    "dist",
    "es",
    "lib"
  ],
  "exports": {
    ".": {
      "types": "./es/index.d.ts",
      "require": "./lib/index.js",
      "import": "./es/index.js"
    },
    "./es/*": "./es/*",
    "./lib/*": "./lib/*",
    "./dist/*": "./dist/*"
  },
  "sideEffects": [
    "dist/*",
    "es/**/style/*",
    "lib/**/style/*",
    "**/*.less"
  ],
}

除了 sideEffects 之外,其它的简单介绍一下:

  • files 就是发布到 npm 时包含的文件
  • mainmoduletypes 分别指向不同环境下的不同包,types 是在 TS 环境下的地址指向
  • exports 字段内和 mainmoduletypes 作用差不多,只不过可以更细粒度的去区分

其实 exports 是用来替代 @babel/plugin-transform-runtimeuseESModules 的(对导出这块我也不是很熟,同样是查资料和摸索出来的)

image

而后边又添加了 ./es/*": "./es/* 等的指向,是因为我在使用按需加载的过程中发现插件无法从 ./es 中读取文件,找不到文件路径,而且编辑器无法给出路径提示(看样子确实是没有指定到文件下),所以才有了这一系列的指向,大家可以去掉之后自行尝试一下。

新增组件实时编译能力

本地使用组件库时,当修改了组件想要看到最新效果,就只能重新 pnpm build 打包,这显然很不方便,好在 gulp 提供了相关的实时编译支持。

gulpfile.js

// 监视 src 目录下的文件变化
function watchFiles() {
  gulp.watch('src/**/*', build);
}

exports.watch = watchFiles;

package.json 添加 dev 命令

  "scripts": {
    "dev": "gulp watch",
    // ...
  },

后续执行 pnpm dev,当 src 下的组件发生变化,就会自动重新编译

安装 storybook 验证打包成果

上一章节我们把 monorepo 的基本文件结构搭建好了,storybook 是直接处于根目录下的子包,此时在该目录下执行初始化命令,暂时不要选择 Next,因为 SSR 环境下目前不确定是否会发生意外情况

pnpm dlx storybook@latest init

安装之后会自动打开页面,这个我们暂时先不关心,因为我们是测试组件的打包,所以先安装组件依赖

pnpm add rclt-components --workspace

storybook 分两个运行界面,一个是 vite + react 默认的模板页面,就是 src 下的文件,这个也可以作为自己的组件库文档进行开发。一个是根据 *.storeis.(tsx|...) 生成的 storybook 文档。

测试组件可用性

App.tsx 中导入组件

image

运行 pnpm dev,打开即可看到一个很丑的按钮 button,因为没有样式,接下来我们做样式的按需导入

测试样式的按需加载

安装 vite-plugin-imp

pnpm i vite-plugin-imp -D

vite.config.ts 中增加配置

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath } from 'node:url';
import vitePluginImp from 'vite-plugin-imp';

export default defineConfig({
  resolve: {
    // sb 打包时,由于组件库为本地目录,sb 找不到路径会打包错误,需使用 alias 指向正确路径
    alias: {
      'rclt-components': fileURLToPath(new URL('../packages/components', import.meta.url)),
    },
  },
  plugins: [
    react(),
    vitePluginImp({
      libList: [
        {
          libName: 'rclt-components',
          style: (name) => `rclt-components/es/${name}/style/index.css`,
        },
      ],
    }),
  ],
});

此时一个漂亮的按钮就成功出来了

image

这么看来在项目结构搭建时可以把 storybook 作为根目录作为项目共享的配置,这样可以直接在 components 文件夹下写 story 文档,结构上更方便查看

编写文档

storybook 的约定文件格式可以在 main.ts 中看到,我们把它进行稍加修改

const config: StorybookConfig = {
  docs: {
    autodocs: true,
  },
  stories: ['../src/**/index.stories.@(js|jsx|mjs|ts|tsx)'],
};
  • autodocs 就是用来全局配置 stories.ts 中的 tags:["autotag"]
  • stories 的路径现在只匹配 index.stories,是为了当文档示例过多时,以 index.stories 作为入口,其余可以导入进来,条理更清晰一点。

删除 stories 下的所有文件,新建 button 文件夹,新建 Button 组件的 story 文档

// storybook/stories/button/index.stories.tsx

import { Button } from 'rclt-components';
import type { Meta, StoryObj } from '@storybook/react';
// export {OtherButton} from "./source/other-button.store"

const meta = {
  title: '基础组件/Button',
  component: Button,
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const ButtonType: Story = {
  name: '按钮类型',
  render: () => (
    <div style={{ display: 'flex', gap: '10px' }}>
      <Button type="default">Default Button</Button>
      <Button type="primary">Primary Button</Button>
      <Button type="danger">Danger Button</Button>
    </div>
  ),
};

export const ButtonSize: Story = {
  name: '按钮大小',
  render: () => (
    <div style={{ display: 'flex', gap: '10px' }}>
      <Button type="primary" size="small">
        Small Button
      </Button>
      <Button type="primary">Default Button</Button>
      <Button type="primary" size="large">
        Large Button
      </Button>
    </div>
  ),
};

执行 pnpm storybook,运行 storybook 文档

image

样式的按需加载也被 storybook 文档享受到了,不需要单独导入 css。

打包 umd 格式的组件 bundler

umd 可以直接在浏览器环境使用,各大组件库基本都提供有 umd 格式的组件产物。

这里使用的是 webpack 打包 umd,没有选择 rollup,大家根据喜好选择即可,这个比较简单。

webpack 打包 bulder

安装 webpack 相关依赖和打包用到的 loader:

pnpm i webpack webpack-cli webpack-merge terser-webpack-plugin babel-loader ts-loader -D

components 下新建 webpack 文件夹,思路如下

  • dist 包提供两个 bundler 产物,一个压缩过的 min.js 和未压缩的 .js
  • dist 包提供两个 css 产物,一个压缩过的 min.css 和未压缩的 .css

webpack 下新建三个文件 webpack.common.jswebpack.dev.jswebpack.prod.js

webpack.common.js 写入以下内容

const path = require('path');

/** @type {import("webpack").Configuration} */
module.exports = {
  bail: true,
  // devtool: 'source-map',
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
  entry: {
    index: path.resolve(__dirname, '../src/index.ts'),
  },
  output: {
    // filename: 'jwstwe-ui.min.js',
    path: path.join(__dirname, '../dist'),
    library: 'rclt',
    libraryTarget: 'umd',
  },
  module: {
    rules: [
      {
        test: /.js(x?)$/,
        use: [{ loader: 'babel-loader' }],
        exclude: /node_modules/,
      },
      {
        test: /.ts(x?)$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
            },
          },
        ],
        exclude: /node_modules/,
      },
    ],
  },

  // 组件库不直接集成 react 和 react-dom
  externals: {
    react: {
      root: 'React',
      commonjs2: 'react',
      commonjs: 'react',
      amd: 'react',
    },
    'react-dom': {
      root: 'ReactDOM',
      commonjs2: 'react-dom',
      commonjs: 'react-dom',
      amd: 'react-dom',
    },
  },
};

webpack.dev.js 实际上就是来打包未压缩版本的产物

const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');

/** @type {import("webpack").Configuration} */
const devConfig = {
  mode: 'development',
  output: {
    filename: 'rclt.js',
  },
};

module.exports = merge(devConfig, commonConfig);

webpack.prod.js 打包压缩版本的产物

const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
const commonConfig = require('./webpack.common');

/** @type {import("webpack").Configuration} */
const prodConfig = {
  mode: 'production',
  output: {
    filename: 'rclt.min.js',
  },
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};

module.exports = merge(prodConfig, commonConfig);

package.json 添加命令 build:dist 命令

  "scripts": {
    // ...
    "build:dist": "webpack --config ./webpack/webpack.dev.js && webpack --config ./webpack/webpack.prod.js",
    // ...
  },

执行 pnpm build:dist,就可以看到生成了 dist 文件夹

image

细节优化

  • webpackbar 打包进度条
  • case-sensitive-paths-webpack-plugin 文件大小写敏感检测
pnpm i webpackbar case-sensitive-paths-webpack-plugin @types/case-sensitive-paths-webpack-plugin -D

webpack.common.js 中添加

const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const WebpackBarPlugin = require('webpackbar');

/** @type {import("webpack").Configuration} */
module.exports = {
  // ...
  plugins: [new CaseSensitivePathsPlugin(), new WebpackBarPlugin()],
};

打包 css

webpack 就算安装 less-loader ,想打包出 css,也是完全没用的,因为 less 样式是完全独立的,组件内部不引入样式,webpack 在打包时,完全找不到使用的 less,自然就打包不出来。

其实也是因为有更简单的方法。

之前是怎么编译 css 的?gulp 使用 gulp-less 将 less 编译成 css 分别输出到产物目录下,那也可以顺便再生成一下 dist的 css

先安装 gulp-concact 合并文件, gulp-cleaner-css 压缩 css(是 gulp-clean-css 的一个维护分支)

pnpm i gulp-concact @types/gulp-concact gulp-cleaner-css -D

完善 gulpfile.js

/**
 * 生成css文件
 */
function less2css() {
  return gulp
    .src(paths.compileStyles)
    .pipe(less()) // 编译 less文件
    .pipe(autoprefixer()) // 根据browserslistrc增加前缀
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.esm))
    .pipe(concat('rclt.css'))
    .pipe(gulp.dest(paths.dest.dist))
    .pipe(cleanCSS()) // 压缩 CSS
    .pipe(concat('rclt.min.css'))
    .pipe(gulp.dest(paths.dest.dist)); // 输出压缩版到 dist
}

此时 dist 的 css 也可以生成了。回到 package.json,将 build 命令里添加 build:dist

  "scripts": {
    "dev": "gulp watch",
    "build": "npm run clean:build && npm run build:types && gulp && npm run build:dist",
    "build:types": "tsc -p tsconfig.json --outDir es  && cpr es lib",
    "build:dist": "webpack --config ./webpack/webpack.dev.js && webpack --config ./webpack/webpack.prod.js",
    "clean:build": "rimraf lib es dist"
  },

执行 pnpm build,如图所示,就大功告成了

image

测试 dist

新建一个 test-dist.html,引入 react、react-dom、babel 的 umd 链接

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="./packages/components/dist/rclt.min.css" />
  </head>
  <body>
    <div id="root"></div>
  </body>
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin></script>
  <script src="./packages/components/dist/rclt.min.js"></script>
  <script type="text/babel">
    const { Button } = rclt;

    function App() {
      return (
        <div style={{ display: 'flex', gap: '10px' }}>
          <Button type="default">Button</Button>
          <Button type="primary">Button</Button>
          <Button type="danger">Button</Button>
        </div>
      );
    }
    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</html>

image

组件库的打包到此结束!

标签:gulp,TypeScript,less,babel,js,React,webpack,组件,css
From: https://www.cnblogs.com/jsonq/p/18542327

相关文章

  • 从零到一构建并打包 React + TypeScript + Less组件库教程(一、项目初始化搭建+代码规
    本系列涉及的内容如下:组件库基础搭建,react+ts+less项目规范,包括但不限于prettier、eslint、stylelint、husky、lint-staged、commitlintpnpmmonorepo+turborepo集成gulp+webpack构建esm、cjs和umdstorybook文档集成此系列不包含发布npm和构建CI流程。......
  • 【React Router】基于 react-router-dom 的路由配置与导航详解
    文章目录一、ReactRouter组件概述1.ReactRouter的作用2.主要组件介绍二、ReactRouter的基本使用1.安装与配置2.配置基础路由3.路由重定向三、嵌套路由与布局1.嵌套路由2.带参路由四、路由守卫与重定向1.路由守卫2.路由重定向五、总结React是现......
  • WebXR与Web组件结合:创建沉浸式Web体验_2024-07-26_16-47-08.Tex
    WebXR与Web组件结合:创建沉浸式Web体验WebXR简介WebXR的定义WebXR是一个WebAPI,它允许开发者创建沉浸式的虚拟现实(VR)和增强现实(AR)体验,直接在网页浏览器中运行。这个API是WebVR的后继者,旨在提供更广泛、更统一的设备支持,以及更强大的功能,如空间追踪、手部追......
  • Node.js v22.6.0新特性:支持 TypeScript 直接运行!
    Node.jsv22.6.0版本已经发布,并且带来了一个开发者们期待已久的新特性——直接运行TypeScript(TS)文件的能力!版本更新亮点Node.jsv22.6.0版本通过--experimental-strip-types标志,实现了对TypeScript的实验性支持。这意味着开发者们现在可以在Node.js环境中直接执行......
  • 浅响应式数据(Shallow Reactive 和 Shallow Ref)
    在Vue3中,shallowReactive和shallowRef是用于创建浅响应式数据的工具。它们与普通的reactive和ref不同,只对对象的第一层属性进行响应式处理,而不会递归地使嵌套对象的属性也变成响应式的。shallowReactive问题:当你有一个嵌套较深的对象,并且你只关心对象的第一层属......
  • 如何将下载的mp4视频嵌入react应用程序?
    将下载的mp4视频嵌入React应用程序可以通过以下步骤实现:将下载的mp4视频文件放置在React应用程序的合适位置,例如在public文件夹下创建一个videos文件夹,并将视频文件放置其中。在React组件中引入视频文件,可以使用<video>标签来嵌入视频。在组件的render方法中,可以使用以下代码来......
  • ❤React-JSX语法认识和使用
    1、JSX基本使用​JSX是React的核心JSX是ES的扩展jsx语法->普通的JavaScript代码->babelReact可以使用JSX的前提和原因:React生态系统支持: 脚手架通常用于构建React应用程序,而JSX是React框架的核心语法之一。因此,脚手架默认支持JSX语法,以便更轻松地编写和管理React组件......
  • 33 张高清大图,带你玩转 KubeSphere 4.1.2 部署与扩展组件安装
    备受瞩目的KubeSphere4.1.2已经正式官宣发布,该版本带来了一个重大优化:增加默认的扩展组件仓库。这一优化改进,让采用全新的KubeSphereLuBan架构的KubeSphere,真正实现了自由打造高度可扩展和可配置的云原生底座。KubeSphere用户仅需要在K8s之上,默认安装清爽简洁、最小化......
  • 【Axure】Arco Design Mobile组件库 - AxureMost
    【Axure】ArcoDesignMobile组件库-AxureMostAxureMost官网【Axure】ArcoDesignMobile组件库-AxureMost【Axure】ArcoDesignMobile组件库/元件库ArcoDesignMobile组件库提供了丰富的基础组件,这些组件设计时考虑了移动设备的特性和用户交互习惯,确保了界面的一......
  • 『玩转Streamlit』--数据展示组件
    数据展示组件在Streamlit各类组件中占据了至关重要的地位,它的核心功能是以直观、易于理解的方式展示数据。本次介绍的数据展示组件st.dataframe和st.table,能够将复杂的数据集以表格、图表等形式清晰地呈现出来,使得用户能够快速把握数据的整体情况和细节特征。1.st.dataframes......