组件库打包配置参考-css打包
组件库打包配置参考-css打包
今天简单讲讲关于组件库打包的css
打包,这里拿arco-design的打包工具arco-cli的1.0
版本来讲解。
开始前
arco-cli
使用的是gulp来组织任务执行的,他能极大的简化构建任务,生态也是及其的庞大,基本业务中的情况都能找到对应的插件。
简单的一些知识可以看看这里。
下面展示的代码可能是笔者更改过的,请勿过分较真(`へ´*)ノ。
在这里贴几个下面会用到的常量
1 |
|
开始
根据源代码,可以看到任务具体分为三个步骤。每个步骤有两个并行的子任务,下面按照任务的顺序一一做介绍。
1 |
|
因为该
arco-scripts
是一个通用的打包cli
,所以本文是基于react
组件库打包进行解析。
copyAsset & copyFileWatched
copyAsset
复制静态文件
此方法比较简单,这里就直接贴代码,看下注释也就能明白。
1 |
|
copyFileWatched
和copyAsset
类似的方法。
复制静态资源和样式文件到es
和lib
目录下。
代码也很简单,直接贴了
1 |
|
compileLess & handleStyleJSEntry
compileLess
编译less
文件。
- gulp-less,其实就是一个对
less
封装的gulp
插件。 - gulp-clean-css,同样是clean-css的封装,用于清理没有使用的
css
样式。 - less-plugin-npm-import,
less
插件,可以在less
文件中引入npm
包样式文件。 - less-plugin-autoprefix,
less
插件,顾名思义,自动补全各个浏览器的前缀。
1 |
|
handleStyleJSEntry
看名字的意思,处理样式的js
入口文件,即index.js
引入样式的文件。
1 |
|
先看一下主方法
1 |
|
接着来一一看下里面的两个方法。
-
compileCssJsEntry
简单解释就是,把源代码里面的每一个组件的样式入口文件index.ts
编译为两个文件index.js
和css.js
。index.js
里面还是原来的内容css.js
里面是引入的文件的经过编译的css
文件
就像下面这样
1
2
3
4
5
6
7
8
9
10
11
12// index.ts内容
import '../../style/index.less';
import './index.less';
// index.js内容
import '../../style/index.less';
import './index.less';
// css.js内容
import '../../style/index.css';
import './index.css';为什么要这么做呢?这样其实就是方便了一些项目可能使用的并不是
less
预编译库,可以直接引入css.js
。然后我们来看下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22async function compileCssJsEntry({
styleJSEntry,
outDirES,
outDirCJS,
}) {
const compile = (module) => {
// xxx
};
try {
const asyncTasks = [];
if (fs.pathExistsSync(outDirES)) {
asyncTasks.push(compile('es'));
}
if (fs.pathExistsSync(outDirCJS)) {
asyncTasks.push(compile('cjs'));
}
await Promise.all(asyncTasks);
} catch (error) {
console.error(error);
}
}这一部分的话简单明了,就是创建了两个任务分别创建了
es
和lib
目录的处理任务。
核心代码的话还是在compile
方法里。下面用到了一个
gulp
插件gulp-replace,是用来做文件内容替换的。
还有一个gulp-rename,顾名思义是做文件重命名的。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
35const replace = require('gulp-replace')
const rename = require('gulp-rename')
const compile = (module) => {
return new Promise((resolve, reject) => {
// styleJSEntry = components/*/style/index.ts
gulp.src(styleJSEntry, {
allowEmpty: true,
// 看着一堆,其实就是 components
base: styleJSEntry.replace(/(\/\*{1,2})*\/style\/index\.[jt]s$/, ''),
})
// 把文件里面的 .less 改成 .css
.pipe(replace('.less', '.css'))
.pipe(
// 源码中已经有注释来说明这一步的目的了,也就是我之前说的那个
// import './index.css' => import './index.css'
// import '../es/Button/style' => import '../es/Button/style/css.js'
replace(/import\s+'(.+(?:\/style)?)(?:\/index.[jt]s)?'/g, (_, $1) => {
const suffix = $1.endsWith('/style') ? '/css.js' : '';
return module === 'es' ? `import '${$1}${suffix}'` : `require('${$1}${suffix}')`;
})
)
.pipe(
rename(function (path) {
// css js
path.basename = 'css';
path.extname = '.js';
})
)
// 输出到指定的目录
.pipe(gulp.dest(module === 'es' ? outDirES : outDirCJS))
.on('end', resolve)
.on('error', reject);
});
} -
injectPackageDepStyle
接着来看一下
injectPackageDepStyle
方法。
首先是参数getComponentDirPattern(['es'])
省流 -> return cwd/es/*
1
2
3
4
5
6
7
8
9
10
11
12function getComponentDirPattern(dirName) {
const pathDir = `${process.cwd()}/${dirName.length > 1 ? `{${dirName.join(',')}}` : dirName[0]}`;
// cwd/es
let pattern = pathDir;
// cwd/es/*/style/index.js
// 也就是上一步被编译好的样式入口文件
if (glob.sync(path.resolve(pathDir, '*/style/index.js')).length) {
// cwd/es/*
pattern = path.resolve(pathDir, './*');
}
return pattern;
}接着是主方法
下面用到一个插件vinyl-fs,用来做文件解析处理。
还有一个through2,文件流处理。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
44const vfs = require("vinyl-fs")
const through = require("through2")
function injectPackageDepStyle(componentEsDirPattern) {
return new Promise((resolve) => {
// cwd/es/*/index.js
const esEntry = path.resolve(componentEsDirPattern, 'index.js');
// ***这里比较奇怪***
if (!fs.existsSync(esEntry)) {
resolve(null);
return;
}
vfs
// 解析所有复合条件的目标文件
.src(esEntry, {
allowEmpty: true,
// /es/*
base: componentEsDirPattern,
})
.pipe(
through.obj(async (file, _, cb) => {
try {
// 这一部分下面接着讲
await Promise.all([
transformStyleEntryContent({
esEntryPath: file.path,
module: 'es',
}),
transformStyleEntryContent({
esEntryPath: file.path,
module: 'cjs',
}),
]);
} catch (error) {
console.error(error);
}
cb(null);
resolve(null);
})
);
});
}上面标注了一段非常奇怪的代码,
fs.existsSync(esEntry)
,实际的esEntry=process.cwd()/es/*/index.js
,但是看下好像fs.existsSync
并不支持*
这类标识符,所以一直会返回false
,它后面的代码根本不会执行,不知道是为什么,可能是我没理解,有懂的可以下面说下👁。
所以我们暂时忽略这串代码,直接走下面的逻辑。-
transformStyleEntryContent
里面用到了
transformStyleEntryContent
这个方法(俄罗斯套娃一样,一层又一层🤷🏻♀️)1
2
3
4
5
6
7
8
9
10
11
12
13async function transformStyleEntryContent({
esEntryPath,
module,
}) {
const replaceStyleEntryContent = async (type) => {
// xxx
};
await Promise.all([
replaceStyleEntryContent('less'),
replaceStyleEntryContent('css'),
]);
}看名字来看就是替换样式入口文件内容的自已。我们接着看
replaceStyleEntryContent
-
replaceStyleEntryContent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const replaceStyleEntryContent = async (type) => {
// 前面方法的参数 es & cjs
const moduleDirName = module === 'es' ? 'es' : 'lib';
// index.js | css.js
const styleEntryFileName =
type === 'less'
? 'index.js'
: 'css.js';
// 把路径修改成置顶模块的路径
// 最终就是 (es | lib)/xx/style/(index | css).js
const styleEntryPath = path
// esEntryPath 就是正在解析的那个文件的目录
// path.dirname(esEntryPath) 就是这个文件的所在的文件夹的位置
// 其实就是 es/xx/style/(index | css).js
.resolve(path.dirname(esEntryPath), `./style/${styleEntryFileName}`)
// 接着把目录改成需要的模块的目录
.replace('/es/', `/${moduleDirName}/`);
// 这个里面有一串比较奇怪的代码,我们单独下面讲解
if (fs.pathExistsSync(styleEntryPath)) {
// xxx
}
}上面的
if
里面还有一串的代码,比较奇怪,所以我们单独放在这里讲
在文件最外层有一个dependenciesCacheMap
变量, 它是一个对象。还有这么一个变量
LIBRARY_PACKAGE_NAME
表示的是你的组件库的包名(我们这里取名your-package-name
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23if(fs.pathExistsSync(styleEntryPath)) {
let styleIndexContent = fs.readFileSync(styleEntryPath, 'utf8');
if (!dependenciesCacheMap[esEntryPath]) {
dependenciesCacheMap[esEntryPath] = await parsePackageImports(
esEntryPath,
LIBRARY_PACKAGE_NAME
);
}
dependenciesCacheMap[esEntryPath].forEach((dep) => {
const depStyleRequirePath = `${LIBRARY_PACKAGE_NAME}/${moduleDirName}/${dep}/style/${styleEntryFileName}`;
if (styleIndexContent.indexOf(depStyleRequirePath) === -1) {
const expression =
module === 'es'
? `import '${depStyleRequirePath}';\n`
: `require('${depStyleRequirePath}');\n`;
styleIndexContent = `${expression}${styleIndexContent}`;
}
});
fs.writeFileSync(styleEntryPath, styleIndexContent);
}首先他有一个
dependenciesCacheMap
用来存储所有已经被解析过的文件模块,避免重复解析。
如果dependenciesCacheMap[esEntryPath]
不存在时,就会使用parsePackageImports
来解析模块。
parsePackageImports
是使用parse-es-import来解析每个es/*/index.js
的引入。
如果是第三方的模块且是LIBRARY_PACKAGE_NAME
,那么就是收集该模块的所有引入。
如果是相对路径模块,则递归调用parsePackageImports
,继续解析该引入的模块。
收集完所有的LIBRARY_PACKAGE_NAME
引入,即parsePackageImports
的返回值,即dependenciesCacheMap[esEntryPath]
。
接着再遍历,拼接出新的引入模块(对应模块的样式),添加到styleEntryPath
内容中。个人理解的话,就是组件当中引入了自己本身的第三方模块,然后自动引入该模块的样式文件。
-
-
distLess & distCss
distLess
把所有组件的入口less
文件自动集中到一个less
文件中,并放到dist
目录下。
大概就是如下这样的结构。
1 |
|
简单流程就是找到components/xx/index.less
文件(所以目录结构有规范,默认认为index.less
为组件等的入口样式文件),路径修改后,字符串拼接出需要的语法,合并到一个字符串中,并写入对应的文件中。
1 |
|
distCss
将上一步(distLess
)生成的less
文件转换成单个css
文件,其目的就是能在umd
模式下能全量引入所有组件的样式。
比如:import 'package-components/dist/css/index.min.css'
1 |
|
结束
关于上面的代码,可以参考下简化的代码,其实就是cv
了arco-scripts
的代码🌶。
结束 🔚。
参考链接
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!