背景
我司之前的组件之间都是现在项目中,后续因为要剥离上传到NPM上,所以选择了用dumi去写组件的说明文档,利用gulp将组件库打包成esm、umd格式。
前置知识
关于ESM、UMD、CJS
- UMD(Universal Module Definition,通用模块定义):UMD模块是一种能够在不同环境下使用的模块。这种模块定义支持AMD、CommonJS和全局变量等不同的模块系统。一般我们在项目中都是使用〈script src="xxxx"> 来挂载。
- CJS(CommonJS):CJS模块是Node.js使用的模块定义方式。这种模块定义使用
require
函数来加载模块,并使用module.exports
或exports
对象来导出模块,但是需注意它的加载是同步的。 - ESM(ECMAScript模块):ESM模块是ECMAScript标准化的模块定义方式。这种模块定义使用
import
关键字来加载模块,并使用export
关键字来导出模块。
简单来说,每个打包格式就是对应于不同规范下的产物,而我们现在前端工程化项目里引入的组件库一般都是ESM格式的打包产物。
关于gulp的一些基本认识
Gulp是一款基于流的自动化构建工具,它可以帮助开发者自动化地执行各种重复的任务,例如编译Sass、压缩JavaScript文件、合并CSS文件等。Gulp使用Node.js编写,并且具有高度的可配置性和灵活性,因此在前端开发中被广泛使用。
gulp使用
- 通过gulp.task()注册任务
- 通过gulp.src()方法获取到想要处理的文件地址
- 把文件流通过pipe方法导入到gulp的插件中
- 把经过插件处理后的流在通过pipe方法导入到gulp.dest()中
- gulp.dest()方法则把流中的内容写入到文件中
遇到的问题
因为之前组件是写在项目中,所以后续打包遇到了很多问题,包括但不仅限于以下几点
- 组件开发的时候文件组织格式与命名没有统一的规范,导致每个组件各有特点
- less文件存在循环引入
- 部分组件中还有png图片,图片体积还不小
- less 文件中别名的处理 and so on
利用gulp处理组件库中的less文件
项目中的less文件在import的时候使用了路径别名,所以在编译less文件的时候需要首先将路径别名替换成相对路径,此处用到的是gulp-style-aliases插件。
funtcion compile() {
// 清理之前打包的产物
rimraf.sync(esDir);
return gulp
// 需要处理的文件
.src(['src/**/style/index.less', 'src/**/style/styles.module.less', 'src/**/style/style.module.less'])
.pipe(
// 替换less中的路径别名
aliases({
'~@components': './src',
}),
)
.pipe(
through2.obj(function (file, encoding, next) {
if (file.isNull()) {
return next(null, file);
}
if (file.isStream()) {
return next(new Error('Streaming not supported'));
}
const cloneFile = file.clone(); // 复制一份打包后可以保留less源文件
const content = file.contents.toString().replace(/^\uFEFF/, '');
cloneFile.contents = Buffer.from(content);
const cloneCssFile = cloneFile.clone();
this.push(cloneFile);
if (file.path.match(/(.*?)\.less$/) || file.path.match(/(.*?)\.module\.less$/)) {
// 在transformLess方法里面处理less
transformLess(cloneCssFile.contents.toString(), cloneCssFile.path)
.then((css) => {
cloneCssFile.contents = Buffer.from(css);
// 处理完后的新less文件 修改文件后缀
cloneCssFile.path = file.path.match(/(.*?)\.module\.less$/)
? cloneCssFile.path.replace(/\.module\.less$/, '.css')
: cloneCssFile.path.replace(/\.less$/, '.css');
this.push(cloneCssFile);
next();
})
.catch((e) => {
console.error(e);
});
} else {
next();
}
}),
)
.pipe(gulp.dest(esDir));
}
transformLess文件中主要是对已经替换过路径别名的less文件进行一些其他处理,包括用post-css和autoprefixer来处理css中的前缀
function transformLess(lessContent, lessFilePath, config = {}) {
const { cwd = process.cwd() } = config;
const resolvedLessFile = path.resolve(cwd, lessFilePath);
const lessOpts = {
paths: [path.dirname(resolvedLessFile)],
filename: resolvedLessFile,
javascriptEnabled: true,
};
return less
.render(lessContent, lessOpts)
.then((result) =>
postcss(lessFilePath.includes('.module.less') ? [autoprefixer, scopedNameGenerator] : [autoprefixer]).process(
result.css,
{ from: lessFilePath },
),
)
.then((r) => {
if (lessFilePath.includes('.module.less')) {
const jsonFile = `${lessFilePath}.json`;
// fs.existsSync 以同步的方法来检测是否存在目录
if (fs.existsSync(jsonFile)) {
fs.unlink(jsonFile, (err) => {
if (err) {
throw err;
}
});
}
}
return r.css;
});
}
module.exports = transformLess;
处理ts文件
主要使用的是gulp-typescript和gulp-path-alias插件 gulp-typescript需要传入一个配置文件
const tsconfig = {
...compilerOptions,
noUnusedParameters: false, // 不能有未使用的变量
noUnusedLocals: false, // 不能有未使用的本地变量
strictNullChecks: true, //严格的null检查
target: 'esnext', // 编译目标
moduleResolution: 'node', // 模块的查找规则
declaration: true, //是否生成声明文件
allowSyntheticDefaultImports: true, //允许默认导入
allowJs: true,
preserveSymlinks: true,
noImplicitAny: false,
paths: {
'@components/*': ['./src/*'],
},
};
gulp-path-alias主要是用于解决ts中配置的路径别名问题
当然还要配置babel,以便于支持jsx和一些其他语法解析
babel({
presets: [
[
'@babel/preset-env',
{
modules: false,
},
],
'@babel/preset-react',
],
plugins: [['@babel/plugin-transform-runtime', { useESModules: true, helpers: true, regenerator: true }]],
}),
打包umd格式
利用gulp将组件库打包成umd格式,需要基于esm产物进行二次打包,这时gulp的入口文件需要指定为整个esm产物中最外层的index.js入口文件(这个文件汇集了所有的组件,通过export分别对外导出)
这里需要使用webpack-stream插件进行处理(一个能允许你在gulp中使用webpack的插件),然后配置一些webpack参数就可以打包成umd了,需要注意的是externals参数,该参数在webpack中提供了从输出的 bundle 中排除依赖,移除第三方依赖,能够大大的减轻umd包的体积大小
function umdWebpack() {
rimraf.sync(umdDir);
return gulp
.src('./dist/es/index.js')
.pipe(
webpackStream(
{
output: {
filename: 'components.js',
library: {
type: 'umd',
name: 'Components',
},
path: path.resolve(__dirname, 'dist/es'),
globalObject: 'this',
},
mode: 'production',
resolve: {
extensions: ['.js', '.json'],
alias: {
// 将 './lib/es' 指定为根路径
'../screen/hooks/useVisible': path.resolve(__dirname, './dist/es/'),
},
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.svg$/,
use: [
{
loader: 'svg-inline-loader',
options: {
removeTags: ['title', 'desc'],
removeSVGTagAttrs: true,
},
},
],
},
],
},
externals: [
{
react: {
commonjs: 'react',
commonjs2: 'react',
amd: 'react',
root: 'React',
},
'react-dom': {
commonjs: 'react-dom',
commonjs2: 'react-dom',
amd: 'react-dom',
root: 'ReactDOM',
},
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: 'lodash',
},
antd: {
commonjs: 'antd',
commonjs2: 'antd',
amd: 'antd',
root: 'antd',
},
},
],
},
webpack,
),
)
.pipe(gulp.dest('./dist/umd/'));
}