Skip to main content

构建配置

配置结构

项目配置文件中的build对象用来控制与构建相关的行为,包括webpackbabelless等。该配置有如下的结构:

// 以下是工具内置了优化的相关第三方库
export type ThirdPartyUse = 'antd' | 'lodash' | 'styled-components' | 'emotion' | 'reflect-metadata' | 'tailwind';

interface BuildStyleSettings {
// 是否将CSS抽取到独立的.css文件中,默认为false,打开这个配置可能导致CSS顺序有问题
readonly extract: boolean;
// 用于编译LESS的变量资源文件列表。每个文件均会被注入到所有的LESS文件前面,作为全局可用的资源
readonly resources: string[];
// 额外的LESS变量,以对象的形式提供,用于less的modifyVars配置
readonly lessVariables: Record<string, string>;
// 启用CSS Modules,默认为true。为true对非第三方代码启用,为false则全面禁用,为函数则通过文件路径自主判断
readonly modules: boolean | ((resoruce: string) => boolean);
}

interface BuildScriptSettings {
// 经过babel处理的文件,默认为true。为true对非第三方代码启用,为false则全面禁用,为函数则通过文件路径自主判断
readonly babel: boolean | ((resoruce: string) => boolean);
// 是否自动引入core-js的相应polyfill,默认为true。如果你使用了其它方式引入polyfill,设置为false即可
readonly polyfill: boolean;
// 是否自动生成组件的displayName,取值为auto时仅在development下生效,关掉后构建的速度会提升一些,产出会小一些,但线上调试会比较麻烦
readonly displayName: boolean | 'auto';
// 最终手动处理babel配置
readonly finalize: (babelConfig: TransformOptions, env: BuildEntry) => TransformOptions | Promise<TransformOptions>;
}

type Severity = 'off' | 'print' | 'warn' | 'error';

// 产物检查的规则配置,为数组的时候,第2个元素是具体的配置
type RuleConfig<T> = 'off' | 'print' | [Severity, T];

type OptionalRuleConfig<T> = Severity | [Severity, T];

// 设置对名称的过滤规则,includs会优先于excludes起作用
interface SourceFilter {
includes?: string[];
excludes?: string[];
}

interface BuildInspectInitialResource {
// 初始加载资源数量,配置值为最大允许数量
readonly count: RuleConfig<number>;
// 初始加载的资源总大小,配置值为最大允许的体积,以字节为单位
readonly totalSize: RuleConfig<number>;
// 初始加载的各资源之间的体积差异,配置值为单个资源的尺寸与所有资源尺寸平均值的差异系数,如0.3指尺寸必须在平均值的0.7-1.3倍之间
readonly sizeDeviation: RuleConfig<number>;
// 禁止在初始加载资源中包含某些第三方依赖,配置值为依赖名称的数组
readonly disallowImports: RuleConfig<string[]>;
}

interface BuildInspectSettings {
readonly initialResources: BuildInspectInitialResource;
readonly duplicatePackages: OptionalRuleConfig<SourceFilter>;
readonly htmlImportable: OptionalRuleConfig<SourceFilter>;
}

type RuleFactory = (buildEntry: BuildEntry) => Promise<RuleSetRule>;

interface InternalRules {
readonly script: RuleFactory;
readonly less: RuleFactory;
readonly css: RuleFactory;
readonly image: RuleFactory;
readonly svg: RuleFactory;
readonly file: RuleFactory;
}

interface BuildInternals {
readonly rules: InternalRules;
}

interface BuildSettings {
// 指定使用的第三方库,会为这些库做特殊的优化,默认会开启antd和lodash,如果自定义这个数组则要手动补上默认的值
readonly uses: ThirdPartyUse[];
// 产出的资源路径前缀
readonly publicPath?: string;
// 是否以第三方库的形式构建,第三方库的构建不使用featureMatrix、不拆分chunk,同时构建产出不带hash、不产出HTML文件
readonly thirdParty: boolean;
// 构建过程中检查代码规范,默认值为true。如无特殊原因,禁止关闭这个开关
readonly reportLintErrors: boolean;
// 生成静态文件的限值,以字节为单位。小于该值的会被编译为DataURI内联,大于该值的会变为单独的文件。默认值为8KB
readonly largeAssetSize: number;
// 应用的标题,用于生成<title>元素
readonly appTitle: string;
// favicon的位置
readonly favicon?: string;
// 在HTML中增加一个放应用的<div>元素,id属性由这个配置指定。如果不配置,就不会自动增加这元素。自定义HTML模板时这个配置失效
readonly appContainerId?: string;
// 构建过程中需要排除的Feature名称,默认排除'dev',其它均会被构建
readonly excludeFeatures: string[];
// 控制样式编译的配置
readonly style: BuildStyleSettings;
// 控制脚本编译的配置
readonly script: BuildScriptSettings;
// 最终手动处理webpack配置,第一个参数的类型并不完全对应Webpack配置,具体见下文
readonly finalize: (webpackConfig: WebpackConfiguration, buildEntry: BuildEntry, internals: BuildInternals) => WebpackConfiguration | Promise<WebpackConfiguration>;
// 配置对最终产出的检查规则
readonly inspect: BuildInspectSettings;
}

虽然上面的类型定义中每个字段都是必须的,但在实际使用时有默认值填充的机制,通常一个应用需要appTitlefavicon配置,其它的配置都可以省略。多数配置请参考上方代码中的注释了解其作用,对于一些比较特殊的自定义扩展点,会在下文中说明。

脚本相关

增加babel编译的文件

默认情况下,仅应用的源码,即src下的文件会经过babel的编译,其它文件将直接由webpack处理。有时某些第三方的包会发布语法比较新的内容,而应用又恰好需要支持比较老旧的浏览器,此时可能需要让babel去处理额外的文件。

我们可以通过build.script.babel配置来指定处理的文件范围,它可以是一个(resource: string) => boolean的函数,接收当前文件的绝对路径,返回true则表示要通过babel处理该文件。

注意:当将这个配置指定为函数时,原有的src下文件经过babel处理的默认逻辑会失败,因此你需要在函数中再额外加上这个判断。

例如,我们知道some-lib这个包需要经过babel处理,再考虑到src下的源码,就可以这样来编写配置:

import path from 'node:path';
import {fileURLToPath} from 'node:url';

const src = path.join(path.dirname(fileURLToPath(import.meta.url)), 'src');

export default configure(
'webpack',
{
build: {
script: {
babel: resource => resource.includes(src) || resource.includes(`node_modules/some-lib/`),
},
},
}
);

控制polyfill生成

在默认配置下,babel会自动分析代码中使用过的新语言特性,并自动生成core-js的引用。默认配置中使用了usage这一选项,你可以参考此处来了解这一行为。

usage这一行为也有自己的缺点,包括但不限于:

  • 会将core-js的引用和其它代码混杂在一起,比较难以单独管理或拆分出来。
  • 随着源码的变化,引入不同的新语言特性时,会额外增加或减少对core-js的引用,导致最终生成的哈希不够稳定。

因此,有些产品会选择使用自定义的polyfill来匹配业务,或全量引入core-js来追求哈希的稳定和长效缓存的可用性。在这种情况下,只需要进行简单地配置就可以关闭babel的相关功能:

export default configure(
'webpack',
{
build: {
script: {
polyfill: false,
},
},
}
);

特殊第三方库的优化

社区上有许多的库需要构建期工具的支持才能取得更好的使用效果。reSKRipt在长远的计划上会精选支持高质量、持续维护、对生产效率有足够帮助的库。但默认打开所有相关的优化必定会拖慢构建速度,因此提供了选项由使用者指定你所用的库。

通过项目配置文件中的build.uses配置可以指定你使用的库,这个属性是一个枚举字符串的数组,当前支持以下值:

  • antd:对antd的导入进行优化,可以参考babel-plugin-import的相关说明。
  • lodash:对lodash的导入进行优化,可以参考babel-plugin-lodash的说明。这个优化只会在production模式下启用。
  • styled-components:对styled-components的使用进行构建期的优化,可以参考babel-plugin-styled-components的相关说明。
  • emotion:对emotion样式解决方案进行处理,这个插件是emotion部分功能的必须依赖使用emotion要求你的react版本在16.14.0以上。
  • reflect-metadata:在构建TypeScript文件中的装饰器语法时,会额外增加对Reflect.metadata相关的代码,主要用于NestJS、或InversifyJS等库。
  • tailwind:在样式处理上引入tailwind的处理。这个声明仅仅让样式处理支持tailwind,但你需要自己安装tailwindcss、生成tailwind.config.js并自行在CSS中通过@tailwind引入相关的样式。

reSKRipt在默认选项下,这一配置的值为['antd', 'lodash'],即默认启用这2个库的相关优化:

// 最终lodash中其它函数都会消失
import {filter, map} from 'lodash';
// 会引入Button组件的源码和样式,其它组件的内容会消失
import {Button} from 'antd';

如果你自定义这个配置,那么默认的antdlodash优化会被取消,你需要自己补充这两个值。假设你在使用着antdlodash的同时,又希望使用styled-components,则可以这么写:

export default configure(
'webpack',
{
build: {
uses: ['antd', 'lodash', 'styled-components'],
},
}
);

而假设你使用ramda代替了lodash,又不希望额外的优化影响构建速度,你也可以自定义这个配置来移除对lodash的处理:

export default configure(
'webpack',
{
build: {
uses: ['antd'],
},
}
);

另外,antdlodash的优化还会与CDN的使用产生冲突,如果你已经决定通过CDN全量引入antdlodash,则需要将这个优化关闭。

扩展babel配置

最后,reSKRipt也允许你在自动生成的babel配置的基础上增加自己的插件,你可以使用build.script.finalize这一函数来扩展。这个函数的第一个参数是最终生成的babel配置,通过修改这一配置就能实现扩展。

如果你需要增加一个插件,比如babel-plugin-macros,那么在安装这个插件后,你可以简单地在plugins中进行追加:

export default configure(
'webpack',
{
build: {
script: {
finalize: babelConfig => {
babelConfig.plugins.push('babel-plugin-macros');
return babelConfig;
},
},
},
}
);

样式相关

提取样式

使用build.style.extract可以将样式提取到独立的.css文件中:

export default configure(
'webpack',
{
build: {
style: {
extract: true,
}
},
}
);

但我们发现这一做法比较容易导致样式的顺序错乱,进一步导致样式优先级不合预期,引起界面错误。考虑到这种顺序的错乱不易在开发过程中发现,所以我们不是很推荐将样式独立出来。

注入less资源

你可以选择一部分.less文件作为全局的资源,并让reSKRipt将它们注入到所有其它.less文件中。在这一功能的帮助下,你可以通过一个.less文件来定义一系列的变量或者mixin,随后在全局任意地使用它们,也不需要每次用到的时候需要手写@import语句。

默认情况下,你的项目中src/styles/*.var.less文件都会被自动注入,这些文件不需要你自定义配置,我们推荐你将全局用到的变量和mixin放到以上规则的文件中,尽量避免做自定义的注入。

你可以使用build.style.resources字符串数组来声明你需要注入的样式文件的路径,每一个路径都必须是绝对路径

import path from 'node:path';

export default configure(
'webpack',
{
build: {
style: {
resources: [
path.join('src', 'styles', 'variables.less'),
path.join('src', 'styles', 'mixins.less'),
],
},
},
}
);

同时值得注意的是,靠reSKRipt来注入一些依赖意味着你的.less文件没有自己声明自己的依赖,它们将无法被单独地通过less来编译,当然这在大多数情况下并不是问题。

还有一点需要额外小心,一定不要在被自动注入的.less文件中写最终会变成CSS的内容,一个常见的错误是把mixin写成了class的样子:

.flex-center {
display: flex;
align-items: center;
justify-content: center;
}

假设这个文件被注入到100个.less中,因为.flex-center在语法上实际是一个有意义的CSS类,所以最终会生成100份的.flex-center选择器,导致样式的体积大大增加。所以你需要严格地按照mixin的定义来写:

// 注意一定别漏了这里的括号
.flex-center() {
display: flex;
align-items: center;
justify-content: center;
}

自定义less变量

你还可以通过build.style.lessVariables来自定义全局的less变量,往往在定制antd主题时用到,你可以参考antd的文档来了解哪些变量可用。比如我们可以简单地替换全局主色来“绿了”antd

export default configure(
'webpack',
{
build: {
style: {
lessVariables: {
'primary-color': '#0aa779',
},
},
},
}
);

控制CSS Modules范围

默认情况下,所有在src目录下的.less.css文件都经过CSS Modules,即最终的CSS类会变成一段带有哈希的全局唯一的名称。

我们建议你保持默认行为,做好样式的隔离有助于你的系统的可维护性。对于少量且可控的情况,你可以使用.global.less.global.css来避开CSS Modules的处理。当文件名不可控且必须放在src下又要脱离CSS Modules时,你可以使用build.style.modules配置来控制范围。

当然与babel配置相同,当你启用了这个配置时,默认的src下全局CSS Modules的逻辑也会消失,你需要精确控制某一个文件。例如你(虽然不知道出于啥原因)在src下引入了bootstrap.css文件,要额外排除它,那么可以考虑这样编写配置:

import path from 'node:path';
import {fileURLToPath} from 'node:url';

const src = path.join(path.dirname(fileURLToPath(import.meta.url)), 'src');

export default configure(
'webpack',
{
build: {
style: {
modules: resource => resource.includes(src) && !resource.endsWith('/bootstrap.css'),
},
},
}
);

关闭代码检查

通常来说,在构建的时候进行代码检查不会造成太大的消耗,并且这对你的项目的代码质量有很大的帮助。reSKRipt默认在本地调试时不会进行代码检查,仅在skr build时做一次全量检查。

如果你对自己的代码十分有信心,并且实在无法接受在构建时代码检查的时间消耗,那么你可以通过以下配置来关闭检查:

export default configure(
'webpack',
{
build: {
reportLintErrors: false,
},
}
);

这将同时关闭对脚本和样式的检查。但我们依然不允许你完全地规避到对代码质量的要求,因此你必须在本地设置一个提交代码的钩子,确保本地的代码质量。

首先,安装husky

npm install --save-dev husky

随后,加上pre-commit的钩子:

npx --no-install husky install \
&& npx --no-install husky add .husky/pre-commit "npx --no-install skr lint --staged --fix --auto-stage"

这样的配置会告诉reSKRipt你已经承诺在每次代码提交时做好检查,此后reportLintErrors: false才会生效。

为什么不用pre-push

当你本地有多个提交时,在pre-push检查出代码规范的问题,它可能并不是由最新的提交引入的。

此时你修复规范问题后,要么生成一个独立的提交,要么合并到最新的提交中,这都会让你的代码历史中有一个提交存在代码规范问题。

或者你选择修复问题后生成新的提交,并用git rebase -i把它合并到之前引入问题的提交中,这一操作非常麻烦且耗时,还容易造成rebase过程中的冲突。

因此,我们强制要求在pre-commit时进行代码检查,避免在事后处理复杂的问题。使用--staged参数可以只检查即将提交的变动,不会消耗太多时间。

自定义调整webpack配置

如果你对reSKRipt生成的webpack并不满意,想要自己做一些调整,可以使用build.finalize来实现。这个配置项是一个函数,第一个参数为最终生成的webpack配置对象,第二个参数为一个BuildEntry对象。

例如,你希望复制一些文件到最终生成的代码中,可以通过copy-webpack-plugin来实现:

import CopyPlugin from 'copy-webpack-plugin';

export default configure(
'webpack',
{
build: {
finalize: webpackConfig => {
webpackConfig.plugins.push(new CopyPlugin({/* 相关配置 */}));
return webpackConfig;
},
},
}
);

或者你想额外处理wasm文件:

export default configure(
'webpack',
{
build: {
finalize: webpackConfig => {
const wasmRule = {
test: /\.wasm$/,
use: 'wasm-loader',
};
webpackConfig.module.rules.push(wasmRule);
return webpackConfig;
},
},
}
);

reSKRipt会保证以下的属性是有值的,不会出现undefined的情况:

  • plugins
  • module.rules
  • resolve.alias
  • optimization

因此,你可以安全地使用webpackConfig.plugins.push(new CopyPlugin())这样的代码,而不需要对plugins属性判空。

调整已有webpack规则

虽然通过build.finalize可以灵活地调整webpack配置,但你几乎不可能直接去修改已有的loader规则,比如你想让所有的图片不再使用默认的url-loader,而是用responsive-loader代替,但你是没办法在webpackConfig.module.rules中找到处理图片的那条规则的。

在这种时候,你只能完全重写整个module.rules配置,我们在build.finalize函数的第三个参数internals中提供了了rules对象来让你进一步复用某些规则,rules中有以下的规则:

  • script:处理.js.jsx.ts.tsx等脚本文件。
  • less:处理.less样式文件。
  • css:处理.css样式文件。
  • image:处理.png.jpg.gif等图片文件。
  • svg:处理.svg文件。
  • file:处理其它二进制文件。

每一个规则都是一个(entry: BuildEntry) => RuleSetRule的函数,因此build.finalize中的第二个参数就起到了作用:

export default configure(
'webpack',
{
build: {
finalize: (webpackConfig, buildEntry, internals) => {
const loadingRules = [
internals.rules.script(buildEntry),
internals.rules.less(buildEntry),
internals.rules.css(buildEntry),
internals.rules.svg(buildEntry),
internals.rules.file(buildEntry),
] as const;
const builtinRules = await Promise.all(rules);

// 需要把整个rules都重写
webpackConfig.module.rules = [
...builtinRules,
// 在上面没有引用rules.image,自定义图片的处理规则
{
test: /\.(jpe?g|png|webp)$/i,
use: 'responsive-loader',
},
];
return webpackConfig;
},
},
}
);

复用现有的loader

承接上文,有时候你在处理一个自定义的文件类型的时候,又想能够尽量复用reSKRipt内置的loader逻辑,这也是可以做到的。build.finalize函数的第三个参数internals中提供了如下2个函数:

type LoaderType =
| 'babel'
| 'style'
| 'css'
| 'cssModules'
| 'postcss'
| 'less'
| 'lessSafe'
| 'url'
| 'img'
| 'worker'
| 'styleResources'
| 'classNames'
| 'cssExtract'
| 'svg'
| 'svgo'
| 'svgToComponent';


function loader(name: LoaderType, buildEntry: BuildEntry): Promise<RuleSetUseItem | null>;

function loaders(names: Array<LoaderType | false>, buildEntry: BuildEntry): Promise<RuleSetUseItem[]>;
}

其中loader函数可以生成一个loader声明,例如loader('css', buildEntry)就可能异步返回类似这样的结构:

{
loader: resolve('css-loader'),
options: {
sourceMap: true,
modules: false,
},
}

这可以直接用在webpackmodule.rules配置中。

note

有一些规则在特写情况下会返回null,所以记得处理空值。

loaders函数则更智能一些,你可以传递多个LoaderType或者false,它会去除其中的false值,将剩余的创建出对应的loader声明,再去除null的部分,返回一个完全可用的定义。

一个典型的场景是你需要处理.sass文件,且希望它们都具备reSKRipt原本的CSS Modules等样式处理能力以及神奇的样式函数功能,那么你就需要借用loaders函数引入类似postcss-loadercss-loaderstyle-loader等:

export default configure(
'webpack',
{
build: {
finalize: (webpackConfig, buildEntry, internals) => {
const sassRule = {
test: /\.s[ac]ss$/,
use: [
...await internals.loaders(['classNames', 'style', 'cssModules', 'postcss'], buildEntry),
'sass-loader',
],
};
webpackConfig.module.rules.push(sassRule);
return webpackConfig;
},
},
}
);
note

请注意,loaders函数中的参数顺序依然是自右向左的,这与webpackloader一致。

检查最终构建产物

在要求比较严格的项目中,有需要对最终产物的组成进行检查,并应用一些自动化的规则,确保如资源数量、大小等符合预期。

你可以使用项目配置文件中的build.inspect来配置构建产物的检查规则,具体的配置结构参考上文。

规则配置

在产物检查的配置中,大部分检查项都可以配置为以下形式:

  • "off":指关闭该项的检查。
  • "print":指仅打印该检查项的结果,但不做任何的阈值判断和拦截。
  • [severity, config]:配置该项的报告类型,以及指定规则检查的阈值。

不同规则的config阈值不同,比如initialResources.count用来检查初始加载的资源数量,那么它的阈值就是个数字,资源数量超过该值时报警。

severity设为"warn"时,会在构建日志中报告,但构建仍然成功。如果值为"error"时,则除了日志报告外,还会使构建进程异常退出。

示例

初始资源检查

假设你的产品并没有使用HTTP/2,考虑到浏览器的单域名并发能力和用户的普遍网速,你的要求如下:

产品打开时,初始加载的资源不能超过6个,总大小不能超过2MB,各资源的体积尽量平均以最大限度利用并发能力。同时产品初始资源不包含任何和图表(echarts)有关的模块,不包含和编辑器(monaco-editorcodemirror)有关的模块。

为了严格控制产品性能,你要求一但违反上面的规则,构建应当失败,开发者需要修复相关问题。则配置如下所示:

export default configure(
'webpack',
{
build: {
inspect: {
initialResources: {
count: ['error', 6],
totalSize: ['error', 2 * 1024 * 1024],
sizeDeviation: ['error', 0.2],
disallowImports: ['error', ['echarts', 'monaco-editor', 'codemirror']],
},
},
},
}
);

检查重复引入的第三方库

由于NPM管理依赖多版本的逻辑,以及不同的第三方库之间依赖关系的未知性,很可能在一个构建的最终产物中,某个包有很多不同版本同时存在。这会造成额外的资源体积、脚本解析和执行时间,对于有副作用的模块更可能导致实际运行出现不可预期的行为。

你可以使用inspect.duplicatePackages来检查这些重复的引入,例如你的要求如下:

检查所有重复引入的依赖包,但对于仅用作开发的tslib这个包和eslint-开头的包不做报警。相关结果只打印出来,不要中断构建的正常进行。

你可以使用如下的配置:

export default configure(
'webpack',
{
build: {
inspect: {
duplicatePackages: ['warn', {excludes: ['tslib', 'eslint-*']}],
},
},
}
);

最后你可能得到类似这样的报告产出:

 W  Found duplicate package immer
at /path/to/project/node_modules/immer
at /path/to/project/node_modules/@huse/methods/node_modules/immer
W Found duplicate package color-name
at /path/to/project/node_modules/color-name
at /path/to/project/node_modules/color-convert/node_modules/color-name

每一条报警信息都会告诉你被重复引入的包名,以及被引入的各个版本所在的绝对路径。

检查微前端兼容性

当下有不少前端系统使用qiankun作为微前端框架进行开发,不过qiankun对你的产出会有一些要求,也有不少的开发者没有及时注意这些限制,直到调试时才在运行时发现应用跑不起来。

其中最为典型的一个问题是,qiankun要求你的入口脚本是HTML中的最后一个<script>标签,如果你“不幸”在作为入口的.js后又插入了一些其它的脚本,那么系统就会无法接入微前端基座了。

为此,我们增加了一个htmlImportable的检查,你可以使用如下配置:

export default configure(
'webpack',
{
build: {
inspect: {
htmlImportable: 'error',
},
},
}
);

如果最终产出的HTML不符合要求,会出现类似的错误并异常退出:

E  The last script in index-stable.html doesn't reference to an entry script, this can break micro-frontend frameworks like qiankun.

如果有一部分产出的HTML是由你自己控制,且不与微前端框架整合,你可以使用includesexcludes来控制被检查的HTML文件:

export default configure(
'webpack',
{
build: {
inspect: {
// 干掉自己生成的
htmlImportable: ['error', {excludes: ['copyright.html', 'about-*.html']}],
},
},
}
);