0%

制定一个统一标准化babel-preset

首先我们要认清应用项目构建和公共构建的差别。作为前端团队,我们构建了很多应用项目,对于一个项目来说,“只要能在需要兼容的环境中跑起来”就达到了基本目的。而对于一个公共库来说,我们的公共库可能被各种环境所引用或需要支持各种兼容需求,因此公共库就要兼容性能和易用性,要注重质量和广泛度。由此看来,公共库理论上构建机制就更加复杂

制定一个企业级公共库的设计原则

这里说的企业级公共库主要是指在企业内复用的公共库,它可以被发布到npm上进行社区共享。也可以在企业内的私有npm中限定范围地共享。总之,企业级公共库是需要在业务中被使用的。一个企业级公共库的设计原则应该包括一下几点。

  • 最快地搭建调试和开发环境
  • 安全地发版维护
  • 公共库文档建设完善
  • 公共库质量有保证
  • 接入和使用负担最小

基于上述原则,在团队里,设计一个公共库前,你需要考虑:

  • 自研公共库还是使用社区已有轮子
  • 公共库的运行环境是什么,这将决定公共库的职责和边界

上述内容并非纯理论原则,而是直接决定了公共库实现的技术选型。比如,为了实现更完善的文档建设,尤其是 UI 组件类文档,可以考虑部署静态组件展示站点,进行组件展示以及用法说明。更智能、工程化的内容,我们可以考虑使用类似 JSDoc 来实现 JavaScript API 文档生成,组件类公共库可以考虑 Storybook 或者 Styleguides 作为标准接入方案。

再比如,我们的公共库适配环境是什么?一般来讲可能需要兼容:浏览器/Node.js/同构环境等。不同环境对应了不同的编译和打包标准,这就需要你进行思考:如果目标是浏览器环境,那么如何才能充分实现性能最优解?如帮助业务方实现 tree-shaking 等优化技术。

同时,为了减轻业务使用负担,作为企业级公共库,以及对应使用这些企业级公共库的应用项目,可以指定标准规范的 babel-preset,保证编译产出的统一。这样一来,业务项目(即使用公共库方)可以以统一的接入标准引入。

制定一个统一标准的babel-preset

企业中,所有公共库或应用项目都使用一套 @xxx/babel-xxx-preset

这里给出一份设计方案,仅供参考

  1. 支持NODE_ENV = ‘development’ | ‘production’ | ‘test’ 三种环境,并有对应的优化
  2. 配置插件默认不开启 Babel loose: true 配置,让插件的行为尽可能地遵循规范,但对有较严重性能损耗或有兼容性问题的情况保留修改入口
  3. 这份设计方案落地后产出,应该支持应用编译和公共库编译,即可以按照 @xxx/babel-preset/app,@xxx/babel-preset/dependencies,@xxx/babel-preset/library 进行区分使用

@xxx/babel-preset/app,@xxx/babel-preset/dependencies 都可以作为编译应用项目的预设使用,但他们也有所差别,具体如下:

  • @xxx/babel-preset/app 负责编译除node_modules外的业务代码
  • @xxx/babel-preset/dependencies 编译node_modules 第三方代码
  • 对于企业级公共库,建议使用标准ES特性发布;对tree-shaking有强烈需求的库,应同时发布ES module格式代码
  • 对于应用编译,使用 @babel/preset-env 同时编译应用代码与第三方库代码。
  • 对于应用,需要对node_modules进行编译,并且为node_modules配置sourceType:’unambiguous’,以确保第三方依赖包中的commonJs模块能够被正确处理
  • 对于应用编译,启用plugin-transform-runtime,避免同样的helper代码被重复注入多个文件,以缩减打包后文件的体积。同时自动注入 regenerator-runtime,避免污染全局变量
  • 注入绝对路径用的 @babel/runtime包中对应的helper

基于以上设计,对于CSR应用的Babel编译流程,预计业务方使用预设为

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
// webpack.config.js
module.exports = {
presets: ['@lucas/babel-preset/app'],
}
// 相关 webpack 配置
module.exports = {
module: {
rules: [
{
test: /\.js$/,
oneOf: [
{
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
configFile: false,
// 使用我们的 preset
presets: ['@lucas/babel-preset/dependencies'],
compact: false,
},
},
],
},
],
},
}

同样按照node_modules进行了区分,对于node_modules第三方依赖,使用@xxx/babel-preset/dependencies预设,同时传入target参数。对于非node_modules的业务代码,使用@xxx/babel-preset/app这个预设,同时设定相应环境 target,@xxx/babel-preset/app内容为:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
const path = require('path')
const {declare} = require('@babel/helper-plugin-utils')
const getAbsoluteRuntimePath = () => {
return path.dirname(require.resolve('@babel/runtime/package.json'))
}
module.exports = ({
targets,
ignoreBrowserslistConfig = false,
forceAllTransforms = false,
transformRuntime = true,
absoluteRuntime = false,
supportsDynamicImport = false,
} = {}) => {
return declare(
(
api,
{
modules = 'auto',
absoluteRuntimePath = getAbsoluteRuntimePath(),
react = true,
presetReactOptions = {},
}
) => {
api.assertVersion(7)
return {
presets: [
[
require('@babel/preset-env').default,
{
useBuiltIns: false,
modules,
targets,
ignoreBrowserslistConfig,
forceAllTransforms,
exclude: ['transform-typeof-symbol'],
},
],
react && [
require('@babel/preset-react').default,
{useBuiltIns: true, runtime: 'automatic', ...presetReactOptions},
],
].filter(Boolean),
plugins: [
transformRuntime && [
require('@babel/plugin-transform-runtime').default,
{
useESModules: 'auto',
absoluteRuntime: absoluteRuntime ? absoluteRuntimePath : false,
},
],
// https://github.com/facebook/create-react-app/issues/4263
[
require('@babel/plugin-proposal-class-properties').default,
{loose: true},
],
require('@babel/plugin-syntax-dynamic-import').default,
!supportsDynamicImport &&
!api.caller(caller => caller && caller.supportsDynamicImport) &&
require('babel-plugin-dynamic-import-node'),
[
require('@babel/plugin-proposal-object-rest-spread').default,
{loose: true, useBuiltIns: true},
],
require('@babel/plugin-proposal-nullish-coalescing-operator').default,
require('@babel/plugin-proposal-optional-chaining').default,
].filter(Boolean),
env: {
development: {
presets: [
react && [
require('@babel/preset-react').default,
{
useBuiltIns: true,
development: true,
runtime: 'automatic',
...presetReactOptions,
},
],
].filter(Boolean),
},
test: {
presets: [
[
require('@babel/preset-env').default,
{
useBuiltIns: false,
targets: {node: 'current'},
ignoreBrowserslistConfig: true,
exclude: ['transform-typeof-symbol'],
},
],
react && [
require('@babel/preset-react').default,
{
useBuiltIns: true,
development: true,
runtime: 'automatic',
...presetReactOptions,
},
],
].filter(Boolean),
plugins: [
[
require('@babel/plugin-transform-runtime').default,
{
useESModules: 'auto',
absoluteRuntime: absoluteRuntimePath,
},
],
require('babel-plugin-dynamic-import-node'),
],
},
},
}
},
)
}