上篇文章讲了咱们的指标,上面拆分解说如何实现目标:
一 生成指标款式目录
后面咱们曾经说了咱们的款式目录构造,回顾一下:
编码目录是这样子:
生成目录这样子:
为什么编码构造和生成构造要这样子能够看上篇文章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;}// 编译lessfunction 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();}// 编译tsfunction 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),
// 编译lessfunction 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.jsfunction 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.jsfunction 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组件编译的配置其实这一章的代码曾经有了组件编译的配置