CMS 公共模块打包实践

2,076 阅读5分钟

背景

最近新搭建了两个 CMS (内容管理)系统,为了减少开发切换项目成本,和降低用户使用成本,两个系统使用了统一的脚手架 antd-pro。

在功能开发的过程中发现,两个系统之间存在很多相同的功能、逻辑。可能 A 项目写一下,后面 B 项目需要同样的实现又得再写一遍。

例如登录页面的 UI、个性化 Table 组件封装、PageLoading 组件、请求封装等等,都是些 ts/tsx 文件,部分组件可能会依赖 less,jpeg/png/svg 等资源。

无需多言,这样的存在肯定是不合理的,如何避免 copy/paste?首先想到的把公共模块抽离出来发布一个 npm package。

常见的打包方案

说到模块,首先想到的就是如何打包,常见的打包工具有一下几种:

工具优点缺点
webpack/rollup没有实现不了的1. 配置麻烦(多个 entry/多个 output,还要保留组件路径)
typescript + babel逻辑清晰,tsc 生成 d.ts + babel copy-files 可以很轻松把组件的 less、d.ts、js 文件生成到指定目录只能处理 js、无法处理 less、静态资源
零配置打包工具 parcel、microbundle零配置场景固定,可配置项少,定制化成本极高

通过简单地对比,要定制化打包组件、还要打包 less/images,基本上只能选 webpack/rollup 了。

打包成 commonjs 规范,编译 lesscss,生成 d.ts 文件。

包目录树大概是这样:

@scope/common
  - package.json
  - dist/
    - assets/
        - logo.jpg
    - components/
        - MyComponent/
            - index.css
            - index.js
            - index.d.ts
    

到时候在项目中使用组件和 Antd 组件单独使用方式一样。手动引入 js 组件,再引入 css 文件

import MyComponent1 from '@scope/common/dist/components/MyComponent1'
import '@scope/common/dist/components/MyComponent/index.css'
import MyComponent2 from '@scope/common/dist/components/MyComponent2'
import '@scope/common/dist/components/MyComponent2/index.css'

常规操作,方案肯定是可行的,但是但是对比抽离模块之前的方式,用起来真的很!不!方!便!原来的用法,直接引入组件就可以了,自动会将组件内部的其他依赖打包,简洁太多了。

import MyComponent1 from '../../components/MyComponent1'
import MyComponent2 from '../../components/MyComponent2'

而且还有另一个问题,原本组件内使用的图片资源怎么办?全部处理成 base64 吗?这样包体积会大幅增加。

非得打包不可吗?

回到最初的需求,抽离公共模块的目的是为了让组件、逻辑得到复用,避免同样的代码散落在各处。这部分代码只是我们从两个管理系统抽离出来的公共代码,不具备全局通用性。

抽离后的用法最好也跟原来的方式一样,以 tsx? 文件为入口,内部的其他依赖都能被正确的 loader 处理。

大胆假设直接发布 typescript、less 代码,由实际使用方来决定如何打包。

当然可以了!,对于打包工具 webpack 来说,需要打包的文件放在哪个目录下还不是都一样。哪些文件使用哪些 loader,是通过 module.rule 中的 test, include, exclude 参数来配置的。

例如一下的配置,webpack 会让 my-project 下,非 node_modules 的代码都经过 babel-loader

module: {
    rules: [
      {
        test: /\.(js|mjs|jsx|ts|tsx)$/,
        include: [
          /my-project/
        ],
        exclude: [
          /node_modules/
        ],
        use: [
          ... babel-loader
        ]
      }
   ]
}

umi(antd-pro 封装的框架) 使用的打包工具就是 webpack,从源码上看,umi@3.0.2 以上版本就会自动处理 node_modules 下的 Typescript

webpackConfig.module

    .rule('ts-in-node_modules')

      .test(/\.(jsx|ts|tsx)$/)

      .include.add(/node_modules/).end()

      .use('babel-loader')

        .loader(require.resolve('babel-loader'))

        .options(babelOpts);

这里用的是 webpack-chian 的语法,它可以通过链式写法生成 webpack 配置。

打脸

原本文章到这里就结束了。当我满心欢喜开发时又出问题了,我将抽离出来的公共包,使用 npm link 到管理项目中后,报错了。

从报错上看,tsx 并没有正确经过 babel-loader,why??? 仔细确认生成的 webpack 配置项之后,将目光锁定在了 webpack 本身。难道是软链引起的?

查阅文档,找到 webpack 软链相关的配置项 resolve.symlinks

Whether to resolve symlinks to their symlinked location. When enabled, symlinked resources are resolved to their real path, not their symlinked location. Note that this may cause module resolution to fail when using tools that symlink packages (like npm link).

默认情况下,webpack 会将软链解析成真实路径(关掉这个配置可能会导致其他问题)。所以问题是,使用 npm link 时,正则表达式 /node_modules/ test 匹配不到这个路径,导致最终公共模块的代码没有经过 babel-loader 处理。

这个报错在 npm publish 之后再 install 下来的情况是不会出现的。

问题明确了,剩下的就是修改 webpack 配置。

umi 如何修改 webpack 配置?

umi 通过 chainWebpack 项修改配置,用的还是 webpack-chain 语法。这里只需要把公共模块的绝对路径加上就行了。

// config/config.ts


chainWebpack(memo) {
  memo.module
    .rule('ts-in-node_modules')
    .include.add(require('path').join(__dirname, '../../packages/'));
  return memo;
}

Done, 现在可以无痛抽离公共模块了~ Happy Coding。

为了方便组件的使用,可以定义 index.tsx 来为做 reexport,而不是深入到组件目录。

// index.tsx reexport
export { MyComponent } from './src/components/MyComponent'

// new usage
import { MyComponent } from '@scope/common'

vs

// old way
import MyComponent from '@scope/common/src/components/MyComponent'

还需要修改 package.json,声明 maintypes

{
    main: './index.tsx',
    types: './index.tsx'
}

最后一个问题,monorepo,不发包直接放在公共目录下可以吗?

项目中我们使用 lerna 来管理多个前端项目,所有代码都在同一个文件夹下,如果不发包而是通过相对路径去引用公共文件夹下的代码可以吗?

- packages/
	- module-a/
	- module-b/
    - commons/

// 在 module-a 中直接通过路径访问
import PageLoading from '../../../commons/PageLoading'

这种形式的问题是,module-a 代码范围超出了它本身应该负责的范畴。并且没有 npm 版本的概念,牵一发而动全身,容易出现【不确定】的更新,还有分支依赖的问题。