# 基础搭建

基于此前 我们分析的监控系统的SDK架构 ,我们决定采用Lerna作为管理多包工具,Rollup作为构建工具,当然,其他常用的基础配置也是少不了的 TypeScript + ESLint + Husky + ESModule

# Lerna

Lerna (opens new window) 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目。

# 初始化项目

建议将Lerna 安装到全局环境中

npm install -g lerna
成功
1

我这里安装完后,lerna版本是V6.6.2,接下来我们使用lerna进行初始化项目

mkdir beaconify && cd beaconify
lerna init
成功
1
2

你的目录结构应该是如下结构:

- beaconify/
  - packages/
  - package.json
  - lerna.json
成功
1
2
3
4
  • packages 是 存放子包的目录,由 package.json 中的 workspaces 字段指定路径,也可以在lerna.json 中指定,默认为 packages

  • lerna.json 是 lerna 项目的配置文件,它包含了多个子包的配置信息。

    • $schema:SON Schema 的一个关键字,用于指定当前 JSON 文件的架构(schema)文件。
    • packages:指定存放子包的目录,默认为 packages。
    • version:指定版本号的格式,默认为 independent,表示每个子包可以有独立的版本号。当设为 fixed 时,所有子包的版本号都必须一致。
    • npmClient:指定使用的 npm 客户端,默认为 npm。
    • command:指定执行命令的方式,默认为 run,表示使用 npm script 的方式来执行命令。
    • useWorkspaces:指定是否启用 yarn workspaces 特性,默认为 false。如果设置为 true,则不需要再使用 lerna bootstrap 安装依赖,而是直接使用 yarn install 安装即可。
    • npmClientArgs:指定传递给 npm 客户端的额外参数。
    • ignore:指定忽略的文件或目录,支持 glob 匹配。

# 添加子包

为方便后续环境的搭建,我们这里先新建两个子包 coreutils,以 core包为例,先执行命令

lerna create core
成功
1

会出现提示让你填写一些资料,我们可以一路回车创建完毕后,手动在子包的 package.json中更改,也可以在创建过程中填写

# 填写包名
package name: (core) @beaconify/core 
# 版本号
version: (0.0.0) 0.0.1
# 描述
description: 
# 关键词 
keywords: 
# 主页
homepage: 
# 开源协议
license: (ISC) MIT
# 入口文件
entry point: (lib/index.js) 
# 绑定的git仓库
git repository: (https://github.com/CrayonPig/beaconify.git) 
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

创建完毕后,目录结构如下

- packages
  - core
    - __tests__ # 测试用例
      - core.test.js
    - lib # 代码目录
      - core.js # 入口
    - package.json
    - README.md
- lerna.json
- package.json
成功
1
2
3
4
5
6
7
8
9
10

同样的,我们创建utils 子包,并lib下的文件都对应package.json 中的 main 入口文件字段的内容,将文件名重命名为index.js

- packages
  - core
    - __tests__ # 测试用例
      - core.test.js
    - lib # 代码目录
      - index.js # 入口
    - package.json
    - README.md
  - utils
    - __tests__ # 测试用例
      - utils.test.js
    - lib # 代码目录
      - index.js # 入口
    - package.json
    - README.md
- lerna.json
- package.json
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 子包调用

我们接下来使用 core 调用 utils的方法,首先在根目录下为 core安装依赖

lerna add @beaconify/utils --scope=@beaconify/core
成功
1

表示为 @beaconify/core 的子包中安装 @beaconify/utils 包作为运行时依赖项。

安装成功后,在根目录中会出现 node_modules , 里面有 @beaconify/utils 包

TIP

当你在一个子包中安装依赖时,Lerna 会自动将该依赖项添加到根目录下的 package.json 文件中,并将该依赖项安装到根目录下的 node_modules 目录中。同时,Lerna 还会在当前子包中创建一个名为 node_modules 的符号链接,指向根目录下的 node_modules 目录。

core/lib/index.js 修改为如下内容

const utils = require('@beaconify/utils')

function core() {
  utils();
  console.log('Hello from core');
}

core()

module.exports = core;
成功
1
2
3
4
5
6
7
8
9
10

utils/lib/index.js 修改为如下内容

function utils() {
  console.log('Hello from utils');
}

module.exports = utils;
成功
1
2
3
4
5

然后通过node执行 core/lib/index.js,成功打印两个日志后,代表引用成功

# 后续安装依赖

通过上述调用后,我们可以正常使用依赖,但后续重新安装依赖或者其他成员使用时,应该需要更简单的方法使用,所以我们要在 package.json 加一个新的命令

"scripts": {
  "postinstall": "lerna link"
},
成功
1
2
3

postinstall 是自带的钩子函数,表示在 npm install 执行完成后自动调用, lerna link 表示将仓库中互相引用的依赖连接起来

有关更多的script 字段,可参考script解析

# Rollup

Rollup (opens new window)是一个 JavaScript 模块打包工具,可以将多个小的代码片段编译为完整的库和应用。常见的如Vue.jsReact.js均是由 Rollup 打包构建的。

# 使用ESModule

使用 ESModule 可以提供更好的模块化支持、静态代码分析、明确的依赖管理、命名空间隔离、动态导入和类型检查等优势。这些好处有助于提高代码的可读性、可维护性和性能,并提供更好的开发体验。除此之外,Rollup本身支持 ESModule,并且支持Tree-Shaking (opens new window),可以对代码进行静态分析,将实际未使用用到的代码剔除。

  1. 将根目录的 package.json 文件的 type 属性改为 module
  2. 将 core 和 utils 包内容改为 ESModule 形式

# 添加执行脚本

  1. 在项目中安装 Rollup, 我这里版本号是 3.21.7
npm install rollup --save-dev
成功
1
  1. 根目录创建 scripts 文件夹,并创建 rollup.dev.js 文件
  export default {
    input: 'packages/core/lib/index.js', // 入口文件
    output: {
      file: './dist/bundle.js', // 出口文件
      format: 'iife', // 生成文件格式,为方便调试暂时先写 iife,意为输出后bundle为自执行函数,适用于 <script> 标签
      name: 'beaconify', // 全局变量名
    },
  }
成功
1
2
3
4
5
6
7
8
  1. 根目录 package.json 添加一个 dev 命令, -c, --config 命令是制定配置文件,如果没有指定,则默认根目录 rollup.config.js
"scripts": {
  "dev": "rollup --config ./scripts/rollup.dev.js"
},
成功
1
2
3

接下来我们执行 npm run dev 命令,可以发现,在根目录下的dist中的确生成 bundle.js 文件,内容如下

import { utils } from '@beaconify/utils';

function core() {
  utils();
  console.log('Hello from core');
}

export { core as default };
成功
1
2
3
4
5
6
7
8

跟我们想象的不太一样, @beaconify/utils 包内的内容并没有被导出,只是做了一个引用。这是因为 Rollup 默认只会对 node_modules 目录中的文件做引用,如果需要导出,要使用额外插件 @rollup/plugin-node-resolve (opens new window),至于为什么不将 node-resolve 作为内置功能? (opens new window)

npm install @rollup/plugin-node-resolve --save-dev
成功
1

修改 rollup.dev.js 文件

import resolve from '@rollup/plugin-node-resolve';

export default {
  input:'packages/core/lib/index.js', // 入口
  output: {
    file:'./dist/bundle.js', // 出口
    format: 'iife',
    name:'beaconify',
  },
  plugins: [resolve()]
}
成功
1
2
3
4
5
6
7
8
9
10
11

再次执行 npm run dev 命令,看到输出文件变成了

function utils() {
  console.log('Hello from utils');
}

function core() {
  utils();
  console.log('Hello from core');
}

export { core as default };
成功
1
2
3
4
5
6
7
8
9
10

虽然我们已经定好标准使用 ESModule ,但我们无法保证项目中使用的所有的第三方库都是 ESModule,为了支持 Commonjs,需安装插件@rollup/plugin-commonjs

npm install @rollup/plugin-commonjs --save-dev
成功
1

rollup.dev.js 中添加

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'packages/core/lib/index.js', // 入口
  output: {
    file: './dist/bundle.js', // 出口
    format: 'iife',
    name: 'beaconify',
  },
  plugins: [
    resolve(),
    commonjs()
  ]
}
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 热更新

为了提高效率,在开发的时候,我们希望 rollup能自动监听文件的改动,并且因为我们 SDK 场景是用于浏览器,所以我们希望能像 webpack 一样,打开页面供我们调试。

针对监听文件的改动,Rollup 为我们提供了命令 -w/--watch, 表示监听 bundle 中的文件并在文件改变时重新构建

"scripts": {
  "dev": "rollup -w --config ./scripts/rollup.dev.js"
},
成功
1
2
3

打开页面调试,我们需要借助 rollup-plugin-serverollup-plugin-livereload, 这两个插件常常一起使用,rollup-plugin-serve 用于启动一个服务器,rollup-plugin-livereload 用于文件变化时,实时刷新页面

npm install rollup-plugin-serve --save-dev
npm install rollup-plugin-livereload --save-dev
成功
1
2

修改 rollup.dev.js 文件

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import serve from 'rollup-plugin-serve'
import livereload from 'rollup-plugin-livereload'

export default {
  input: 'packages/core/lib/index.js', // 入口
  output: {
    file: './dist/bundle.js', // 出口
    format: 'iife',
    name: 'beaconify',
  },
  plugins: [
    resolve(),
    commonjs(),
    serve({
      open: true,
      contentBase: ['example', 'dist'],  //服务器启动的文件夹,默认是项目根目录,需要在该文件下创建index.html
      port: 8020   //端口号,默认10001
    }),    
    livereload('dist')   //watch dist目录,当目录中的文件发生变化时,刷新页面
  ]
}
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

同时 根目录创建example文件夹,并添加index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<script src="./bundle.js"></script>
<body>
  
</body>
</html>
<script>
  beaconify()
</script>
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后执行 npm run dev 命令,可以发现可以自动打开浏览器,并且 控制台已经有输出

控制台输出

我们手动更改 coreutils 的内容后,控制台的输出也会随之改变

# TypeScript

为了提高代码维护性和可读性,我们将引入 TypeScript,此项功能依赖 typescirpttslibrollup-plugin-typescript2

npm install rollup-plugin-typescript2 typescript tslib --save-dev
成功
1

rollup.dev.js 文件中,添加ts相关内容

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import serve from 'rollup-plugin-serve'
import livereload from 'rollup-plugin-livereload'
import typescript from 'rollup-plugin-typescript2';

const extensions = [
  '.js',
  '.ts'
]

export default {
  input: 'packages/core/lib/index.ts', // 入口
  output: {
    file: './dist/bundle.js', // 出口
    format: 'iife',
    name: 'beaconify',
  },
  extensions,
  plugins: [
    resolve(extensions),
    commonjs(),
    serve({
      open: true,
      contentBase: ['example', 'dist'],  //服务器启动的文件夹,默认是项目根目录,需要在该文件下创建index.html
      port: 8020   //端口号,默认10001
    }),    
    livereload('dist'),   //watch dist目录,当目录中的文件发生变化时,刷新页面
    typescript({
      extensions
    })
  ]
}
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

extensions 是用于配置解析模块文件的扩展名的选项。通过指定 extensions,你可以告诉 Rollup 在解析模块时考虑哪些文件扩展名。

然后将 core 和 utils 包内容改为 ts 形式,在项目根目录添加 tsconfig.json,该文件具体配置字段描述参见tsconfig.json字段

{
  "compilerOptions": {
    "target": "ESNext", // 目标语言的版本
    "module": "ESNext", // 生成代码的模板标准
    "importHelpers": true,  // 通过tslib引入helper函数,文件必须是模块
    "moduleResolution": "node16", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
    "skipLibCheck": true, // 跳过对声明文件的类型检查
    "esModuleInterop": true, // 允许export=导出,由import from 导入
    "resolveJsonModule": true, // 是否允许直接导入 JSON 文件作为模块。
    "allowSyntheticDefaultImports": true, // 允许在导入模块时使用合成的默认导入。
    "sourceMap": true, // 生成目标文件的sourceMap文件
    "outDir": "dist", // 指定输出目录
    "baseUrl": ".", // 解析非相对模块的基地址,默认是当前目录
    "lib": ["esnext", "DOM"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
  },
  // 指定一个匹配列表,支持 glob 通配符(属于自动指定该路径下的所有ts相关文件)
  "include": [
    "packages/**/*.ts",
    "packages/**/*.d.ts",
  ],
  // 指定一个排除列表,支持 glob 通配符(include的反向操作)
  "exclude": ["node_modules"]
}
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

以上步骤完成后,我们查看 coreutils 包的引用,会发现ts报错

找不到模块“@beaconify/utils”或其相应的类型声明。ts(2307)
成功
1

这是因为ts将@beaconify/utils视为第三方模块,而不是本地资源文件。会去查找他的.d.ts文件,但我们并没有这个文件。解决也很简单,在tsconfig.json 加相关的路径引用

{
  "compilerOptions": {
    // 配置模块解析时的路径映射
    "paths": {
      "@beaconify/utils": [
        "./packages/utils/lib"
      ]
    }
  },
}
成功
1
2
3
4
5
6
7
8
9
10

此时再去看,ts已经正常运行在我们项目中了

# ESLint

ESLint 是一个基于代码风格的代码检查工具,可以检查不规范的代码。

我们首先安装 ESLint 本体 、 rollup 检测插件 @rollup/plugin-eslint 以及我们自己的Eslint规则 eslint-plugin-guide

eslint-plugin-guide 的规则可参见eslint-plugin-guide

npm install eslint @rollup/plugin-eslint eslint-plugin-guide --save-dev
成功
1

TIP

不要使用 rollup-plugin-eslint 插件,该插件不支持一些ESLint 规则,会报错

然后根目录新建 .eslintrc 文件

{
  "root": true,
  "env": {
    "node": true
  },
  "globals": {
    "NodeJS": "readonly",
    "Window": "readonly",
  },
  "plugins": [
    "guide"
  ],
  "extends": [
    "plugin:guide/base",
    "plugin:guide/typeScript"
  ],
  "rules": {
  }
}
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

为什么不使用 `.eslintrc.js` 文件?

ESLint 在处理配置文件时,默认将其视为 CommonJS 模块。但是,当配置文件所在的目录的最近的 package.json 文件中包含 "type": "module" 的声明时,ESLint 将其视为 ES 模块文件。就会产生冲突。

此时我们通过编辑器查看代码,已经有了报错提醒,但是我们使用rollup的时候还没有报错,所以继续配置 rollup.dev.js

import eslint from '@rollup/plugin-eslint';
export default {
  // ...
  plugins: [
    eslint({
      include: ['packages/**/*.ts'],
      throwOnError: true // 有报错则中止进程
    })
  ]
}
成功
1
2
3
4
5
6
7
8
9
10

重新运行,就可以看到报错提醒了

# Babel

由于是监控类SDK项目,对代码体积有一定的要求,所以暂时先不用 Babel,减少不必要的polyfill,尤其是公司项目都是vue、react的情况下,不需要兼容低版本浏览器,如果后续确实有需求,我们可以单开一个polyfill插件做兼容。