关于javascript:基于rolluptypescriptgulpless搭建react-前端组件库二

4次阅读

共计 15481 个字符,预计需要花费 39 分钟才能阅读完成。

上篇文章讲了咱们的指标,上面拆分解说如何实现目标:

一 生成指标款式目录

后面咱们曾经说了咱们的款式目录构造,回顾一下:
编码目录是这样子:

生成目录这样子:

为什么编码构造和生成构造要这样子能够看上篇文章 react 组件库搭建(一)
这部分其实是 gulp 实现的:
首先建设 gulp 执行入口文件夹 gulpfile.js,而后建 index.js 作为 gulp 入口(build 前面再讲):

gulp 的管道流思维对于构建来说十分的便当:

const gulp = require('gulp');
const rimraf = require('rimraf');
var minimatch = require('minimatch');
const less = require('gulp-less');
const glob = require('glob');
// const lessImport = require('gulp-less-import');
const rename = require('gulp-rename');
const concat = require('gulp-concat');
// const gulpIf = require('gulp-if');
const autoprefix = require('less-plugin-autoprefix');
const alias = require('gulp-path-alias');

const path = require('path');

const {buildScript, buildBrowser, styleScriptBuild} = require('./build');
const {getProjectPath} = require('../utils/project');

const outputDirName = './dist';
const outputDir = getProjectPath(outputDirName);
const umdDir = getProjectPath(outputDirName + '/dist');
const esDir = getProjectPath(outputDirName + '/es');
const cjsDir = getProjectPath(outputDirName + '/lib');

// less 全局变量文件
const varsPath = getProjectPath('./src/components/style/index.less');

function globArray(patterns, options) {
    var i,
        list = [];
    if (!Array.isArray(patterns)) {patterns = [patterns];
    }
    patterns.forEach(function(pattern) {if (pattern[0] === '!') {
            i = list.length - 1;
            while (i > -1) {if (!minimatch(list[i], pattern)) {list.splice(i, 1);
                }
                i--;
            }
        } else {var newList = glob.sync(pattern, options);
            newList.forEach(function(item) {if (list.indexOf(item) === -1) {list.push(item);
                }
            });
        }
    });
    return list;
}

// 编译 less
function compileLess(cb, outputCssFileName = 'ti.css') {gulp.src(['src/**/style/**/*.less', 'src/style/**/*.less'])
        .pipe(
            alias({
                paths: {'~@': path.resolve('./src'),
                },
            }),
        )
        .pipe(gulp.dest(esDir)) // 拷贝一份 less es
        .pipe(gulp.dest(cjsDir)) // 拷贝一份 less cjs
        .pipe(
            less({plugins: [autoprefix],
                globalVars: {hack: `true; @import "${varsPath}"`,
                },
            }),
        )
        .pipe(rename(function(path) {
                return {
                    ...path,
                    extname: '.css',
                };
            }),
        )
        .pipe(gulp.dest(esDir)) // 输入 css es
        .pipe(gulp.dest(cjsDir)) // 输入 css cjs
        .pipe(concat(outputCssFileName))
        .pipe(gulp.dest(umdDir));
    cb();}

// 编译 ts
function compileTypescript(cb) {
    const source = [
        'src/**/*.tsx',
        'src/**/*.ts',
        'src/**/*.d.ts',
        '!src/**/__test__/**',
        '!src/**/style/*.ts',
    ];

    const tsFiles = globArray(source);

    buildScript(
        tsFiles,
        {
            es: esDir,
            cjs: cjsDir,
        },
        cb,
    )
        .then(() => {cb();
        })
        .catch(err => {console.log('---> build err', err);
        });
    // 单文件输入
    buildBrowser('src/index.ts', umdDir, cb);
    cb();}

// 提供给 babel-import-plugin 应用的款式脚本文件解决
function styleScriptTask(cb) {const files = glob.sync('src/**/style/*.ts');
    styleScriptBuild(files, { es: esDir, cjs: cjsDir});
    cb();}

// 清空源文件
function removeDist(cb) {rimraf.sync(outputDir);
    cb();}

exports.default = gulp.series(
    removeDist,
    gulp.parallel(compileLess, styleScriptTask, compileTypescript),
);

咱们把上局部代码拆分成几局部,从导出看 gulp.series 是 gulp 工作程序执行的 api,gulp.parallel 是 gulp 工作同时执行的 api:

exports.default = gulp.series(
    removeDist,
    gulp.parallel(compileLess, styleScriptTask, compileTypescript),
);

removeDist 看名字就晓得这一步是在移除文件,编译文件之前先清掉之前的编译文件:

// 清空源文件 gulp 工作
function removeDist(cb) {rimraf.sync(outputDir);
    cb();}

rimraf 是 nodejs 库,用它来清理文件,而后做上面的同时工作,先看前两个和款式文件解决相干的工作

gulp.parallel(compileLess, styleScriptTask, compileTypescript),
// 编译 less
function compileLess(cb, outputCssFileName = 'ti.css') {gulp.src(['src/**/style/**/*.less', 'src/style/**/*.less'])
        .pipe(
            alias({
                paths: {'~@': path.resolve('./src'),
                },
            }),
        )
        .pipe(gulp.dest(esDir)) // 拷贝一份 less es
        .pipe(gulp.dest(cjsDir)) // 拷贝一份 less cjs
        .pipe(
            less({plugins: [autoprefix],
                globalVars: {hack: `true; @import "${varsPath}"`,
                },
            }),
        )
        .pipe(rename(function(path) {
                return {
                    ...path,
                    extname: '.css',
                };
            }),
        )
        .pipe(gulp.dest(esDir)) // 输入 css es
        .pipe(gulp.dest(cjsDir)) // 输入 css cjs
        .pipe(concat(outputCssFileName))
        .pipe(gulp.dest(umdDir));
    cb();}

这两步就是在拷贝 less 文件到 es,cjs 输入目录

.pipe(gulp.dest(esDir)) // 拷贝一份 less es
        .pipe(gulp.dest(cjsDir)) // 拷贝一份 less cjs

这一步就是 less 的变量笼罩,globalVars 不须要干掉它,因为它是变量笼罩,我却误把它当成全局变量应用,只有在开发环境会全局存在,编译之后只是笼罩变量不会注入到所有文件中

.pipe(
            less({plugins: [autoprefix],
                globalVars: {hack: `true; @import "${varsPath}"`,
                },
            }),
        )

这一步是在更改文件后缀,生成一个同目录的 css 文件也就是一个 less 文件对应一个 css 同名文件用于对 css 的反对,path 就是以后编写环境下的 path 门路如 src/omponents/checkbox/style/index.less 通过上面解决之后就变成 src/omponents/checkbox/style/index.css

.pipe(rename(function(path) {
                return {
                    ...path,
                    extname: '.css',
                };
            }),
        )

输入 css 到 es,cjs 下的 style 目录,拿 src/components/checkbox/style/index.less 为例会输入如下文件
dist/es/components/checkbox/style/index.css
dist/cjs/components/checkbox/style/index.css

 .pipe(gulp.dest(esDir)) // 输入 css es
 .pipe(gulp.dest(cjsDir)) // 输入 css cjs

这两步是在生成 umd 须要的款式文件只输入 css

// gulp-concat 插件将管道中的文件都合并到 outputCssFileName 文件中
.pipe(concat(outputCssFileName))
        .pipe(gulp.dest(umdDir)); // 输入文件到 umdDir 目录

将所有 css 文件合并输入到 umdDir 目录咱们的是在 dist/dist 目录文件名字就是 outputCssFileName 变量。那么咱们的款式脚本怎么生成的呢,也就是下图的文件提供给 babel-import-pugin 应用的文件

生成 style 文件中的款式脚本

// 提供给 babel-import-plugin 应用的款式脚本文件解决
function styleScriptTask(cb) {
    // 匹配到源码中的款式入口
    const files = glob.sync('src/**/style/*.ts');
    styleScriptBuild(files, { es: esDir, cjs: cjsDir});
    cb();}

第二个 gulp 工作生成款式脚本文件。首先通过 glob.sync 去匹配到所有的源码款式脚本入口,就如下图这个文件:

而后通过 styleScriptBuild 这个函数解决,这个函数外面是应用 rollup 编译输入的:

const rollup = require('rollup');
const {babel} = require('@rollup/plugin-babel');
const alias = require('@rollup/plugin-alias');
const resolve = require('@rollup/plugin-node-resolve');
const replace = require('rollup-plugin-replace');
// const typescript = require('@rollup/plugin-typescript');
const typescript = require('rollup-plugin-typescript2');
const common = require('@rollup/plugin-commonjs');
const jsx = require('rollup-plugin-jsx');
const less = require('rollup-plugin-less');
const {uglify} = require('rollup-plugin-uglify');
const analyze = require('rollup-plugin-analyzer');

const {nodeResolve} = resolve;
const fs = require('fs');
const path = require('path');
const {getProjectPath} = require('../utils/project');
const varsPath = getProjectPath('./src/components/style/index.less');

function mkdirPath(pathStr) {
    let projectPath = '/';
    const pathArr = pathStr.split('/');
    for (let i = 0; i < pathArr.length; i++) {projectPath += (i === 0 || i === 1 ? '':'/') + pathArr[i];
        if (!fs.existsSync(projectPath)) {
            if (projectPath.indexOf('ti-component/dist') >= 0 &&
                !fs.existsSync(projectPath)
            ) {fs.mkdirSync(projectPath);
            }
        }
    }
    return projectPath;
}

// 是否是浏览器中运行的脚本
function isBrowserScriptFormat(dir) {return dir.indexOf('umd') >= 0;
}

// 是否是导出款式的脚本文件
function isStyleScript(path) {
    return (path.match(/(\/|\\)style(\/|\\)index\.ts/) ||
        path.match(/(\/|\\)style(\/|\\)index\.tsx/) ||
        path.match(/(\/|\\)style(\/|\\)index\.js/) ||
        path.match(/(\/|\\)style(\/|\\)index\.jsx/)
    );
}

// 解决须要间接应用 css 的状况
function cssInjection(content) {
    return content
        .replace(/\/style\/?'/g,"/style/css'") // 默认导入 index 的都转换为导入 css
        .replace(/\/style\/?"/g,'/style/css"')
        .replace(/\.less/g, '.css');
}

// 替换导入 less 脚本中的带有 js 后缀的字符串
function replaceLessScript(code) {if (code.indexOf('.less.js') >= 0) {return code.replace(/\.less.js/g, '.less');
    }
    return code;
}

// 创立导入 css 的脚本名为 css.js
function createCssJs(code, filePath, dir, format) {if (isBrowserScriptFormat(format)) return;
    const icode = replaceLessScript(code);
    const content = cssInjection(icode);
    const cssDir = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, '');
    const styleJsDir = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, '');
    const cssJsPath = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, 'css.js');
    const styleJsPath = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, 'index.js');
    mkdirPath(cssDir);
    mkdirPath(styleJsDir);
    fs.writeFile(cssJsPath, content, function(err) {if (err) {console.log('--------->write file err', err);
        }
    });
    fs.writeFile(styleJsPath, icode, function(err) {if (err) {console.log('--------->write file err', err);
        }
    });
}

/**
 *@desc: 获取 rollup 输出打包配置
 *@Date: 2021-02-18 10:43:08
 *@param {Object} inputOptionOverride 笼罩 input 配置
 *@param {Array} additionalPlugins 新增的插件
 *@param {object} tsConfig
 *@return {void}
 */
function getRollUpInputOption(inputOptionOverride = {},
    tsConfig = {},
    additionalPlugins = [],) {const external = ['react', 'react-dom'];
    const babelOptions = {exclude: ['**/node_modules/**'],
        babelHelpers: 'bundled',
        presets: [
            // "stage-3",
            '@babel/preset-env',
            '@babel/preset-react',
            '@babel/preset-flow',
        ],
        extensions: ['tsx', 'ts', 'js', 'jsx'],
        plugins: [
            '@babel/transform-react-jsx',
            // ['@babel/plugin-transform-runtime', { useESModules: true}],
            [
                '@babel/plugin-proposal-class-properties',
                {loose: true,},
            ],
            [
                '@babel/plugin-proposal-decorators',
                {legacy: true,},
            ],
        ],
    };
    const onAnalysis = ({bundleSize}) => {console.log(`Bundle size bytes: ${bundleSize} bytes`);
        return;
    };
    const inputOptions = {
        external,
        plugins: [common(),
            nodeResolve({extensions: ['.js', '.jsx', '.ts', '.tsx', '.less'],
            }),
            alias({
                entries: [
                    {
                        find: '@',
                        replacement: path.resolve('./src'),
                    },
                    {
                        find: '~@',
                        replacement: path.resolve('./src'),
                    },
                ],
            }),
            replace({stylePre: JSON.stringify('ti'),
                'process.env.NODE_ENV': JSON.stringify('production'),
            }),
            less({
                option: {
                    globalVars: {
                        'theme-color': '#136BDE',
                        hack: `true; @import "${varsPath}"`,
                    },
                },
                output: false,
            }),
            typescript({
                tsconfigDefaults: {include: ['./src/**/*.ts', './src/**/*.tsx'],
                    compilerOptions: {lib: ['es5', 'es6', 'dom'],
                        // exclude: ['./src/**/style/*.ts'],
                        target: 'ES6',
                        // typeRoots: ["./types"],
                        moduleResolution: 'node',
                        module: 'ES6',
                        jsx: 'react',
                        allowSyntheticDefaultImports: true,
                        ...tsConfig,
                    },
                },
            }),
            babel(babelOptions),

            jsx({
                factory: 'React.createElement',
                extensions: ['js', 'jsx', 'tsx'],
            }),
            analyze({onAnalysis, skipFormatted: true, stdout: true}),
            ...additionalPlugins,
        ],
        ...inputOptionOverride,
    };
    return inputOptions;
}

// 编译生成 babel-import-plugin 应用的款式脚本
exports.styleScriptBuild = async function(files, outputConf) {
    const outputOptions = [
        {
            // file: outputPath,
            format: 'cjs',
            dir: outputConf.cjs,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // 不导入其余模块代码
        },
        {
            // file: outputPath,
            format: 'esm',
            dir: outputConf.es,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // 不导入其余模块代码
        },
    ];
    const bundle = await rollup.rollup(
        getRollUpInputOption(
            {
                input: files,
                treeshake: false,
            },
            {declaration: true,},
        ),
    );
    for (const outputOption of outputOptions) {const { output} = await bundle.generate(outputOption);
        for (const chunkOrAsset of output) {if (chunkOrAsset.type === 'chunk') {if (isStyleScript(chunkOrAsset.fileName)) {
                    createCssJs(
                        chunkOrAsset.code,
                        chunkOrAsset.facadeModuleId,
                        outputOption.dir,
                        outputOption.format,
                    );
                }
            }
        }
    }
    await bundle.close();};

// 组件 es cjs 标准编译输入
exports.buildScript = async function(inputPaths, outputConf) {
    // 输入格局
    const outputOptions = [
        {
            // file: outputPath,
            format: 'cjs',
            dir: outputConf.cjs,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // 不导入其余模块代码
        },
        {
            // file: outputPath,
            format: 'esm',
            dir: outputConf.es,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // 不导入其余模块代码
        },
    ];
    for (const outputOption of outputOptions) {
        const bundle = await rollup.rollup(
            getRollUpInputOption(
                {
                    input: inputPaths,
                    treeshake: true,
                },
                {declaration: true,},
            ),
        );
        await bundle.generate(outputOption);
        await bundle.write(outputOption);
        await bundle.close();}
};

// 打包成一个文件
exports.buildBrowser = async function(entryPath, outputDir, cb) {
    const outputOption = {
        file: outputDir + '/index.js',
        format: 'umd',
        // dir: outputDir, preserveModulesRoot: 'src', preserveModules: true,
        name: 'ti',
        exports: 'named',
        globals: {
            react: 'React', // 单个 打包须要裸露的全局变量
            'react-dom': 'ReactDOM',
        },
    };
    const bundle = await rollup.rollup(
        getRollUpInputOption(
            {
                input: entryPath,
                treeshake: true,
            },
            {},
            [uglify()],
        ),
    );
    await bundle.generate(outputOption);
    await bundle.write(outputOption);
    await bundle.close();
    cb();};

代码很多咱们先只看款式解决局部相干

// 编译生成 babel-import-plugin 应用的款式脚本
exports.styleScriptBuild = async function(files, outputConf) {
    const outputOptions = [
        {
            // file: outputPath,
            format: 'cjs',
            dir: outputConf.cjs, // 指标输入目录
            preserveModulesRoot: 'src',
            preserveModules: true, // 同源输入
            exports: 'named', // 导入形式命名导入
            hoistTransitiveImports: false, // 不导入其余模块代码也就是不讲 import 引入的代码打包到一个文件
        },
        {
            // file: outputPath,
            format: 'esm',
            dir: outputConf.es,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // 不导入其余模块代码
        },
    ];
    const bundle = await rollup.rollup(
        getRollUpInputOption(
            {
                input: files,
                treeshake: false,
            },
            {declaration: true,},
        ),
    );
    for (const outputOption of outputOptions) {const { output} = await bundle.generate(outputOption);
        for (const chunkOrAsset of output) {if (chunkOrAsset.type === 'chunk') {if (isStyleScript(chunkOrAsset.fileName)) {
                    createCssJs(
                        chunkOrAsset.code,
                        chunkOrAsset.facadeModuleId,
                        outputOption.dir,
                        outputOption.format,
                    );
                }
            }
        }
    }
    await bundle.close();};

outputOptions 是 rollup 的输入配置,咱们须要输入两种标准 cjs,es 标准。应用 rollup 的 js api,rollup.rollup 进行编译,看看这个函数:

getRollUpInputOption(
                {
                    input: inputPaths,
                    treeshake: true,
                },
                {declaration: true,},
     )

这是提取进去的获取 rollup 输出配置的函数,因为打包组件也须要应用所以提取进去,留神这里的 treeshake,也就是树摇,也称为依赖树。应用打包工具的同学应该不生疏,在变异款式脚本的时候须要敞开,因为咱们的款式文件在编码的时候没有被任何文件导入,咱们是应用的 babel-import-plugin 注入的,如果不敞开那么 rollup 的依赖剖析会认为这个文件没有被依赖属于冗余文件不须要编译输入,那么你编译进去的款式脚本就是空文件。那么 getRoollUpInputOption 这个函数就是 rollup 入口参数配置:

/**
 *@desc: 获取 rollup 输出打包配置
 *@Date: 2021-02-18 10:43:08
 *@param {Object} inputOptionOverride 笼罩 input 配置
 *@param {Array} additionalPlugins 新增的插件
 *@param {object} tsConfig
 *@return {void}
 */
function getRollUpInputOption(inputOptionOverride = {},
    tsConfig = {},
    additionalPlugins = [],) {const external = ['react', 'react-dom'];
    const babelOptions = {exclude: ['**/node_modules/**'],
        babelHelpers: 'bundled',
        presets: [
            // "stage-3",
            '@babel/preset-env',
            '@babel/preset-react',
            '@babel/preset-flow',
        ],
        extensions: ['tsx', 'ts', 'js', 'jsx'],
        plugins: [
            '@babel/transform-react-jsx',
            // ['@babel/plugin-transform-runtime', { useESModules: true}],
            [
                '@babel/plugin-proposal-class-properties',
                {loose: true,},
            ],
            [
                '@babel/plugin-proposal-decorators',
                {legacy: true,},
            ],
        ],
    };
    const onAnalysis = ({bundleSize}) => {console.log(`Bundle size bytes: ${bundleSize} bytes`);
        return;
    };
    const inputOptions = {
        external,
        plugins: [common(),
            nodeResolve({extensions: ['.js', '.jsx', '.ts', '.tsx', '.less'],
            }),
            alias({
                entries: [
                    {
                        find: '@',
                        replacement: path.resolve('./src'),
                    },
                    {
                        find: '~@',
                        replacement: path.resolve('./src'),
                    },
                ],
            }),
            replace({stylePre: JSON.stringify('ti'),
                'process.env.NODE_ENV': JSON.stringify('production'),
            }),
            less({
                option: {
                    globalVars: {
                        'theme-color': '#136BDE',
                        hack: `true; @import "${varsPath}"`,
                    },
                },
                output: false,
            }),
            typescript({
                tsconfigDefaults: {include: ['./src/**/*.ts', './src/**/*.tsx'],
                    compilerOptions: {lib: ['es5', 'es6', 'dom'],
                        // exclude: ['./src/**/style/*.ts'],
                        target: 'ES6',
                        // typeRoots: ["./types"],
                        moduleResolution: 'node',
                        module: 'ES6',
                        jsx: 'react',
                        allowSyntheticDefaultImports: true,
                        ...tsConfig,
                    },
                },
            }),
            babel(babelOptions),

            jsx({
                factory: 'React.createElement',
                extensions: ['js', 'jsx', 'tsx'],
            }),
            analyze({onAnalysis, skipFormatted: true, stdout: true}),
            ...additionalPlugins,
        ],
        ...inputOptionOverride,
    };
    return inputOptions;
}

须要的同学能够去看 rollup 文档,做过工程配置的同学看一下应该就明确是在干什么,就是做 ts jsx tsx less 的变异,以及 babel 的配置,门路别名,编译入口等一些列配置
再回到 styleScriptBuild 这个函数中

for (const outputOption of outputOptions) {const { output} = await bundle.generate(outputOption);
        for (const chunkOrAsset of output) {if (chunkOrAsset.type === 'chunk') {if (isStyleScript(chunkOrAsset.fileName)) {
                    createCssJs(
                        chunkOrAsset.code,
                        chunkOrAsset.facadeModuleId,
                        outputOption.dir,
                        outputOption.format,
                    );
                }
            }
        }
    }
    await bundle.close();

依据配置的输入标准,进行脚本打包

if (chunkOrAsset.type === 'chunk') {if (isStyleScript(chunkOrAsset.fileName)) {
                    createCssJs(
                        chunkOrAsset.code,
                        chunkOrAsset.facadeModuleId,
                        outputOption.dir,
                        outputOption.format,
                    );
                }
            }

这个中央 isStyleScript 是本人定义的,因为所有的款式脚本规定都在 style 文件之下所以,做了一下断定过滤只有款式脚本才做 createCssJs 的解决,createCssJs 其实就是在生成 style/css.js 文件:

// 创立导入 css 的脚本名为 css.js
function createCssJs(code, filePath, dir, format) {if (isBrowserScriptFormat(format)) return;
    const icode = replaceLessScript(code);
    const content = cssInjection(icode);
    const cssDir = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, '');
    const styleJsDir = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, '');
    const cssJsPath = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, 'css.js');
    const styleJsPath = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, 'index.js');
    mkdirPath(cssDir);
    mkdirPath(styleJsDir);
    fs.writeFile(cssJsPath, content, function(err) {if (err) {console.log('--------->write file err', err);
        }
    });
    fs.writeFile(styleJsPath, icode, function(err) {if (err) {console.log('--------->write file err', err);
        }
    });
}

下一章是 rollup 组件编译的配置其实这一章的代码曾经有了组件编译的配置

正文完
 0