npm package: @jlchain/fe-pack
Description: 前端统一打包解决方案
Author: Futurer.J
Latest version: 1.1.0

# 统一打包服务 fe-pack

## 一、npm工具

> yarn add @jlchain/fe-pack
> 或者
> npm install @jlchain/fe-pack

## 二、fe-pack的约定

* typescript应用
* react应用
* 使用css(适合编写通用样式)、less(带有 css module，适合编写组件样式)、less(编译node_modules下的less文件，不带有css module，适合加载ant类型的组件)
* [可配置修改] 默认入口文件为src/main.tsx
* [可配置修改] 默认document.title 取值为 package.json中的description
* [可配置修改] 默认端口号为8080
* [可配置修改]webpack打包时解析别名(resolve.alias)，与tsconfig.json中的compilerOptions.paths配置一致

## 三、fe-pack做了哪些事

### 1. 提供3个实用命令

* 开发模式：`fe-pack start`

> 支持子命令:
> * --log     会输出所有请求信息
> * --disk      磁盘模式，会将编译结果输出到项目根目录下的dist文件夹中
> * --analyzer  分析模式，会分析编译结果，并以图形方式展现



* 发布模式：`fe-pack build`

> 支持子命令:
> * --analyzer  分析模式，会分析编译结果，并以图形方式展现



* 启动服务：`fe-pack serve`



### 2. 目前fe-pack已支持的能力

* react应用

* 资源服务

  * 访问日志
  * 资源压缩
  * 静态资源
  * 接口代理 proxy
  * 服务集群 cluster(仅生产模式)

* 单入口打包

  * 开发编译、压缩编译、dll编译(方便的dll拆包能力)

  * 自动选取端口号避免端口占用问题

  * less 文件预编译

  * 自动定位浏览器页面

  * 远程访问开发环境(程序会自动获取当前主机ip地址，并在编译结束时显示在终端，方便告诉你的伙伴)

  * 开发模式接口代理 proxy

  * 支持插件plugins

  * 开发编译热重启

    ```ts
    // 开发编译监听的文件
    const watchList = [
      'feconfig.dev.js',
      'tsconfig.json',
      'package.json',
      'babel.config.js',
      '.babelrc',
    ];
    ```

### 3. fe-pack 的配置文件

所有的根配置都是可选的，你不配置，应用也能正常启动。

这些配置都是为了让应用具有更多的能力。

#### 1) 开发配置

```ts
// feconfig.dev.js 很多时候会在开发环境修改，建议使用.gitignore，禁止提交git
interface IUserConf {
  // 应用标题，这个内容将会成为页面的tab名称、左侧菜单上面的应用标题栏内容，不配置时默认从package.json的description字段获取
  title?: string
  // 基准端口号，如果这个端口已被占用将自动选取其他空闲端口号，默认8080
  port?: number
  /**
   * 入口文件，默认为 main.tsx，无论入口文件命名如何，编译后的文件都是 main.js
   * fe-pack将会根据baseUrl和entryFile综合计算入口文件的完整路径
   * 单入口打包模式下，默认完整路径为 src/main.tsx
   */
  entryFile?: string
  /**
   * 项目入口文件根路径，默认为 src
   * fe-pack将会根据baseUrl和entryFile综合计算入口文件的完整路径
   * 单入口打包模式下，默认完整路径为 src/main.tsx
   */
  baseUrl?: string
  // 应用使用的mvvm框架: react(default) / vue，目前仅支持react
  mvvm?: 'react' | 'vue'
  // 打包模式: single(default): 单入口打包，multiple: 多入口打包，目前仅支持single
  mode?: 'single' | 'multiple'
  /**
   * 自定义插件
   */
  plugins?: ((context: IfeContext) => void)[]
}
```

```ts
/**
 * plugin可用的上下文api
 */
export interface IfeContext {
  /**
   * 获取当前node环境 dev / qa / pre / prod
   */
  getEnv: () => IEnvEnum
  /**
   * 获取命令行指令
   */
  getCommands: () => string[]
  /**
   * 获取配置内容，只读
   */
  getConfig: () => any
  /**
   * 注册命令
   * 仅支持
   * fe-pack start
   * fe-pack build
   * 下面的子命令
   * 比如 fe-pack start --log 中，--log就是使用 registCommand 注入的
   * fe-pack serve 暂时不支持注入命令
   */
  registCommand: (executor: () => [string, string] | Promise<[string, string]>) => void
  /**
   * 配置webpack的钩子，返回的json会自动合并到webpack配置中
   */
  configureWebpack: (executor: () => webpack.Configuration | Promise<webpack.Configuration>) => void
  /**
   * 编译前钩子
   */
  beforeCompile: (hook: () => void | Promise<void>) => void
  /**
   * 编译后钩子
   */
  afterCompile: (hook: () => void | Promise<void>) => void
  /**
   * 服务启动前的钩子，插件可以使用app注册任意中间件
   */
  beforeServer: (executor: (app: Express) => void | Promise<void>) => void
  /**
   * 服务监听端口后的钩子
   */
  afterServerListening: (hook: () => void | Promise<void>) => void
}
```

##### 1.1)plugin插件系统

在fe-pack中，一个接受上下文并使用这个上下文进行扩展功能定义的函数，就是一个plugin。

这个plugin返回值必须是void，所以plugin不支持异步写法，但是所有的上下文钩子，都支持异步写法。

* 内置plugin

fe-pack已经提供一些内置plugin，用来实现基本功能：

```ts
/**
 * 内置fe plugins
 */
export const plugins = [
  // 打包目标文件夹预删除功能
  predelPlugin,
  // 写入磁盘功能
  diskPlugin,
  // 打包分析功能
  analyzerPlugin,
  // 请求日志功能
  logPlugin,
  // less编译功能
  lessCompilePlugin,
  // 服务基础功能:(gzip/cookie/健康检查/静态资源服务/...)
  serverBasePlugin,
];
```

一个简单的内置plugin实现方式:

```ts
/**
 * 编译结果写入磁盘插件，根据--disk命令决定
 * 仅对开发模式生效，压缩模式固定写入磁盘
 * @param context
 * @returns
 */
export function diskPlugin(context: IfeContext) {
  const {
    getEnv, getCommands, registCommand, configureWebpack,
  } = context;
  const env = getEnv();
  if (env !== IEnvEnum.dev) return;
  registCommand(() => ['--disk', '磁盘模式，会将编译结果输出到项目根目录下的dist文件夹中']);
  const commands = getCommands();
  if (!commands.includes('--disk')) return;
  configureWebpack(() => ({
    devServer: {
      writeToDisk: true,
    },
  }));
}
```

* 用户plugin

fe-pack 目前提供了2个用户plugin，来实现更高级的功能：

```ts
// 支持进行dll拆包
dllPlugin(option: IDllConfig | IDllConfig[])
// 支持自定义proxy代理
proxyPlugin(conf: IProxy | Record<IEnvEnum, IProxy>)
```

对于用户plugin，一个例子:

```ts
// feconfig.dev.js
module.exports = {

  // ...other configs

  plugins: [
    dllPlugin([
      {
        name: 'rbase',
        include: [
          'react',
          'react-dom',
        ],
      },
      {
        name: 'tools',
        include: 'moment',
      },
    ]),
    proxyPlugin({
      '/v1': {
        target: 'http://xxx.demo.com',
        changeOrigin: true,
        secure: false,
      },
    }),
  ],
};
```

由于plugin可能需要根据当前环境来决定具体行为，某些plugin可以使用如下的方式配置:

```ts
// feconfig.js
module.exports = {

  // ...other configs

  plugins: [
    proxyPlugin({
      qa: proxyconf('qa'),
      pre: proxyconf('pre'),
      prod: proxyconf('prod'),
    }),
  ],
};
```

* 自定义plugin

如果上面所有内置plugin都无法满足你的项目需要，你可以编写自定义plugin，比如：

```ts
module.exports = {
  plugins: [
    (context) => {
      const {
        getEnv,
        getCommands,
        getConfig,
        registCommand,
        configureWebpack,
        beforeCompile,
        afterCompile,
        beforeServer,
        afterServerListening,
      } = context;
      // 判断当前环境
      const env = getEnv();
      const isDev = env === 'dev';
      // 注入monaco需要的语言
      configureWebpack(() => ({
        plugins: [
          new MonacoWebpackPlugin({
            languages: ['json', 'shell', 'python', 'java', 'javascript', 'typescript'],
          }),
        ],
      }));
      // 注册指令
      registCommand(() => ['--debug', '调试模式']);
      // 获取当前命令
      const commands = getCommands();
      // 判断是否是调试模式
      const isDebug = commands.includes('--debug');
      // 添加服务中间件
      beforeServer((app) => {
        app.use((req, res) => {
          // todo something
        });
      });
    },
  ],
};
```



#### 2) tsconfig.json

对于fe-pack来说tsconfig.json文件是必须的，这意味着你的应用必须是一个ts应用

fe-pack会根据你的ts配置中的 `compilerOptions.paths` 配置自动生成  `webpack`  `alias ` 别名

不配置将不会生成别名

#### 3) package.json

fe-pack会观察package.json中的一些配置，来确定具体行为：

* `description` : 应用描述，用户如果没有在 `feconfig.js` 配置 `title` ，fe-pack会降级使用description
* `dependencies` : fe-pack会根据你的依赖包，自动确定应用使用的mvvm框架，所以你可以不配置mvvm属性

#### 4) babel.config.js 或者 .bablerc

fe-pack 支持babel零配置

fe-pack 会观察 babel配置，来确定具体行为

如果用户配置了babel，fe-pack会将用户配置与默认配置**合并**

如果用户不配置babel，fe-pack会降级使用默认配置，默认配置如下:

```ts
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3,
      },
    ],
    '@babel/preset-typescript',
    '@babel/preset-react',
  ],
  plugins: [
    '@babel/plugin-transform-runtime',
  ],
  env: {
    dev: {
      plugins: [
        ['react-refresh/babel', { skipEnvCheck: true }],
      ],
    },
  },
};
```

该默认配置决定了**应用必须是react**，并没有指定更多定制化场景，比如你如果想要使用antd，你的.babelrc可以配置如下：

```ts
{
  "plugins": [
    ["babel-plugin-import", {
      "libraryName": "antd",
      "style": true
    }, "antd"], // 如果你想要使用antd
    ["@babel/plugin-proposal-decorators", { "legacy": true }] // 如果你想要使用decorator
  ]
}
```

### 4. 你还需要在应用中配置哪些内容

* package.json

  ```json
  {
    //...
    "scripts": {
      "start": "fe-pack start",
      "build": "fe-pack build",
      "serve": "fe-pack serve"
    },
    //...
  }
  ```

* .gitignore

  ```text
  # ...
  feconfig.dev.js
  ```

* .eslintrc.js
推荐使用`@jlchain/fe-lint-react`

* .stylelintrc.js
推荐使用`@jlchain/fe-lint-react`包下的`@jlchain/fe-lint-react/stylelint`