和白昼一块儿歌唱,和黑夜一块儿作梦。——纪伯伦javascript
作前端,不是在折腾就是在折腾的路上。html
不一样的场景咱们有不一样的应对方案,业务和通用组件的开发也有所差别,这篇文章借助Ant Design,一块儿体悟大厂在开发相似通用组件或类库时,如何定义规范,如何实施协同开发方案,怎么把控开发过程等。到正文前,先来看看咱们封装这样一个库前须要作那些约定和准备。前端
既然是通用组件或者库,就离不开一下几点:java
以上五个步骤是咱们开发并发布组件或库的核心流程,如下,咱们深刻到每个步骤,深究实现原理node
咱们先看一下项目的架构react
开发UI组件库的项目构建有以下两个痛点:webpack
看到以上两个问题,结合咱们开发,能够推测出预览项目和打包须要两套不一样打包编译机制,可是在项目中通常只能使用一种打包方式,即:webpack配置只有一个或一套区分编译环境的文件。因此咱们考虑这两种场景下使用两种不一样方式进行打包处理,最终咱们选用的方案是:bisheng、antd-tools,这里作一个解释,bisheng 是一个使用React轻松将符合约定的Markdown文件经过转换生成SPA网页的框架;antd-tools 定义了ant-design组件库打包相关的处理方案。git
bisheng的处理流程以下图(搜索微信公众号:JavaScript全栈 ,观看视频讲解)github
基本配置
const path = require('path'); const CSSSplitWebpackPlugin = require('css-split-webpack-plugin').default; const replaceLib = require('@ant-design/tools/lib/replaceLib'); const isDev = process.env.NODE_ENV === 'development'; const usePreact = process.env.REACT_ENV === 'preact'; function alertBabelConfig(rules) { rules.forEach(rule => { if (rule.loader && rule.loader === 'babel-loader') { if (rule.options.plugins.indexOf(replaceLib) === -1) { rule.options.plugins.push(replaceLib); } // eslint-disable-next-line rule.options.plugins = rule.options.plugins.filter( plugin => !plugin.indexOf || plugin.indexOf('babel-plugin-add-module-exports') === -1, ); // Add babel-plugin-add-react-displayname rule.options.plugins.push(require.resolve('babel-plugin-add-react-displayname')); } else if (rule.use) { alertBabelConfig(rule.use); } }); } module.exports = { port: 8001, hash: true, source: { components: './components', docs: './docs', changelog: ['CHANGELOG.zh-CN.md', 'CHANGELOG.en-US.md'], }, theme: './site/theme', htmlTemplate: './site/theme/static/template.html', themeConfig: { categoryOrder: { 'Ant Design': 0, 原则: 7, Principles: 7, 视觉: 2, Visual: 2, 模式: 3, Patterns: 3, 其余: 6, Other: 6, Components: 1, 组件: 1, }, typeOrder: { Custom: -1, General: 0, Layout: 1, Navigation: 2, 'Data Entry': 3, 'Data Display': 4, Feedback: 5, Other: 6, Deprecated: 7, 自定义: -1, 通用: 0, 布局: 1, 导航: 2, 数据录入: 3, 数据展现: 4, 反馈: 5, 其余: 6, 废弃: 7, }, docVersions: { '0.9.x': 'http://09x.ant.design', '0.10.x': 'http://010x.ant.design', '0.11.x': 'http://011x.ant.design', '0.12.x': 'http://012x.ant.design', '1.x': 'http://1x.ant.design', '2.x': 'http://2x.ant.design', }, }, filePathMapper(filePath) { if (filePath === '/index.html') { return ['/index.html', '/index-cn.html']; } if (filePath.endsWith('/index.html')) { return [filePath, filePath.replace(/\/index\.html$/, '-cn/index.html')]; } if (filePath !== '/404.html' && filePath !== '/index-cn.html') { return [filePath, filePath.replace(/\.html$/, '-cn.html')]; } return filePath; }, doraConfig: { verbose: true, }, lessConfig: { javascriptEnabled: true, }, webpackConfig(config) { // eslint-disable-next-line config.resolve.alias = { 'antd/lib': path.join(process.cwd(), 'components'), 'antd/es': path.join(process.cwd(), 'components'), antd: path.join(process.cwd(), 'index'), site: path.join(process.cwd(), 'site'), 'react-router': 'react-router/umd/ReactRouter', 'react-intl': 'react-intl/dist', }; // eslint-disable-next-line config.externals = { 'react-router-dom': 'ReactRouterDOM', }; if (usePreact) { // eslint-disable-next-line config.resolve.alias = Object.assign({}, config.resolve.alias, { react: 'preact-compat', 'react-dom': 'preact-compat', 'create-react-class': 'preact-compat/lib/create-react-class', 'react-router': 'react-router', }); } if (isDev) { // eslint-disable-next-line config.devtool = 'source-map'; } alertBabelConfig(config.module.rules); config.module.rules.push({ test: /\.mjs$/, include: /node_modules/, type: 'javascript/auto', }); config.plugins.push(new CSSSplitWebpackPlugin({ size: 4000 })); return config; }, devServerConfig: { public: process.env.DEV_HOST || 'localhost', disableHostCheck: !!process.env.DEV_HOST, }, htmlTemplateExtraData: { isDev, usePreact, }, }; 复制代码
该文件定义了,如何将指定Markdown文件按何种规则转换为预览网页。
定义完文件,咱们只须要执行 npm start
便可运行预览项目,执行 npm start
其实就是执行了以下的命令
rimraf _site && mkdir _site && node ./scripts/generateColorLess.js && cross-env NODE_ENV=development bisheng start -c ./site/bisheng.config.js
复制代码
antd-tools负责组件的打包、发布、提交守卫、校验等工做
antd-tools run dist
antd-tools run compile
antd-tools run clean
antd-tools run pub
antd-tools run guard
复制代码
每一个命令的功能在咱们讲解到对应流程时详细介绍。
本项目使用 Typescript
,组件单元测试使用 jest
结合 enzyme
。具体用例咱们以Button为例来说解。(搜索微信公众号:JavaScript全栈 ,观看视频讲解)
it('should change loading state instantly by default', () => { class DefaultButton extends Component { state = { loading: false, }; enterLoading = () => { this.setState({ loading: true }); }; render() { const { loading } = this.state; return ( <Button loading={loading} onClick={this.enterLoading}> Button </Button> ); } } const wrapper = mount(<DefaultButton />); wrapper.simulate('click'); expect(wrapper.find('.ant-btn-loading').length).toBe(1); }); 复制代码
记得我刚入门编程那会儿,大环境生态尚未如今友好,相似eslint的工具也没有眼下这么易用,说不定同事就上传一些他本身都不能读懂的代码,怎么办?
咱们为了把控质量,代码在本地git commit前,须要检查一下代码是否按约定的代码风格书写,若是不能经过检查,则不容许commit。
咱们借助 husky 在咱们commit时进行指定操做,只需下载husky,并在package.json中配置
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
}
复制代码
hooks定义了咱们要处理时间的钩子,意图很明显,咱们想在commit前,执行指定操做。代码的检查咱们借助 pretty-quick 。
如此一来,当咱们更改了文件,并经过git管理文件版本,执行 git commit
时,该钩子就会进行处理,pretty-quick检查经过则提交成功,不然失败。
关于组件打包,单独封装了一个工具库来处理——antd-tools,咱们顺着package.json给我透露的信息,去分析整个流程,相关启动命令以下
"build": "npm run compile && npm run dist",
"compile": "antd-tools run compile",
"dist": "antd-tools run dist",
复制代码
compile
和 dist
命令配置见项目根路径下 .antd-tools.config.js
function finalizeCompile() { if (fs.existsSync(path.join(__dirname, './lib'))) { // Build package.json version to lib/version/index.js // prevent json-loader needing in user-side const versionFilePath = path.join(process.cwd(), 'lib', 'version', 'index.js'); const versionFileContent = fs.readFileSync(versionFilePath).toString(); fs.writeFileSync( versionFilePath, versionFileContent.replace( /require\(('|")\.\.\/\.\.\/package\.json('|")\)/, `{ version: '${packageInfo.version}' }`, ), ); // eslint-disable-next-line console.log('Wrote version into lib/version/index.js'); // Build package.json version to lib/version/index.d.ts // prevent https://github.com/ant-design/ant-design/issues/4935 const versionDefPath = path.join(process.cwd(), 'lib', 'version', 'index.d.ts'); fs.writeFileSync( versionDefPath, `declare var _default: "${packageInfo.version}";\nexport default _default;\n`, ); // eslint-disable-next-line console.log('Wrote version into lib/version/index.d.ts'); // Build a entry less file to dist/antd.less const componentsPath = path.join(process.cwd(), 'components'); let componentsLessContent = ''; // Build components in one file: lib/style/components.less fs.readdir(componentsPath, (err, files) => { files.forEach(file => { if (fs.existsSync(path.join(componentsPath, file, 'style', 'index.less'))) { componentsLessContent += `@import "../${path.join(file, 'style', 'index.less')}";\n`; } }); fs.writeFileSync( path.join(process.cwd(), 'lib', 'style', 'components.less'), componentsLessContent, ); }); } } function finalizeDist() { if (fs.existsSync(path.join(__dirname, './dist'))) { // Build less entry file: dist/antd.less fs.writeFileSync( path.join(process.cwd(), 'dist', 'antd.less'), '@import "../lib/style/index.less";\n@import "../lib/style/components.less";', ); // eslint-disable-next-line console.log('Built a entry less file to dist/antd.less'); } } 复制代码
咱们深刻到 antd-tools
,改包在node_modules/@ant-design/tools,处理过程是交由 gulp 的,见gulpfile.js
// 编译处理 function compile(modules) { rimraf.sync(modules !== false ? libDir : esDir); const less = gulp .src(['components/**/*.less']) .pipe( through2.obj(function(file, encoding, next) { this.push(file.clone()); if ( file.path.match(/(\/|\\)style(\/|\\)index\.less$/) || file.path.match(/(\/|\\)style(\/|\\)v2-compatible-reset\.less$/) ) { transformLess(file.path) .then(css => { file.contents = Buffer.from(css); file.path = file.path.replace(/\.less$/, '.css'); this.push(file); next(); }) .catch(e => { console.error(e); }); } else { next(); } }) ) .pipe(gulp.dest(modules === false ? esDir : libDir)); const assets = gulp .src(['components/**/*.@(png|svg)']) .pipe(gulp.dest(modules === false ? esDir : libDir)); let error = 0; const source = ['components/**/*.tsx', 'components/**/*.ts', 'typings/**/*.d.ts']; // allow jsx file in components/xxx/ if (tsConfig.allowJs) { source.unshift('components/**/*.jsx'); } const tsResult = gulp.src(source).pipe( ts(tsConfig, { error(e) { tsDefaultReporter.error(e); error = 1; }, finish: tsDefaultReporter.finish, }) ); function check() { if (error && !argv['ignore-error']) { process.exit(1); } } tsResult.on('finish', check); tsResult.on('end', check); const tsFilesStream = babelify(tsResult.js, modules); const tsd = tsResult.dts.pipe(gulp.dest(modules === false ? esDir : libDir)); return merge2([less, tsFilesStream, tsd, assets]); } // 生成打包文件处理 function dist(done) { rimraf.sync(getProjectPath('dist')); process.env.RUN_ENV = 'PRODUCTION'; const webpackConfig = require(getProjectPath('webpack.config.js')); webpack(webpackConfig, (err, stats) => { if (err) { console.error(err.stack || err); if (err.details) { console.error(err.details); } return; } const info = stats.toJson(); if (stats.hasErrors()) { console.error(info.errors); } if (stats.hasWarnings()) { console.warn(info.warnings); } const buildInfo = stats.toString({ colors: true, children: true, chunks: false, modules: false, chunkModules: false, hash: false, version: false, }); console.log(buildInfo); // Additional process of dist finalize const { dist: { finalize } = {} } = getConfig(); if (finalize) { console.log('[Dist] Finalization...'); finalize(); } done(0); }); } 复制代码
如此完成组件打包操做,具体细节讲解见微信公众号:JavaScript全栈
咱们都有一个感觉,每次发包都胆战心惊,准备工做充分了吗?该build的build了吗?该修改的确认过了吗?无论咱们多么当心,仍是会出现一些差错,因此咱们能够在发布包以前定义一些约定规则,只有这些规则经过,才可以进行发布。这是咱们须要借助 npm
提供的钩子 prepublish
来处理发布前的操做,处理的操做即是定义于 antd-tools
中指定的逻辑。咱们一样看到上面看到的 gulpfile.js
。
gulp.task( 'guard', gulp.series(done => { function reportError() { console.log(chalk.bgRed('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')); console.log(chalk.bgRed('!! `npm publish` is forbidden for this package. !!')); console.log(chalk.bgRed('!! Use `npm run pub` instead. !!')); console.log(chalk.bgRed('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')); } const npmArgs = getNpmArgs(); if (npmArgs) { for (let arg = npmArgs.shift(); arg; arg = npmArgs.shift()) { if (/^pu(b(l(i(sh?)?)?)?)?$/.test(arg) && npmArgs.indexOf('--with-antd-tools') < 0) { reportError(); done(1); return; } } } done(); }) ); 复制代码
package.json中的scripts定义
"prepublish": "antd-tools run guard",
"pub": "antd-tools run pub",
复制代码
当咱们执行 npm publish
时 antd-tools run guard
执行,阻止咱们直接使用发布命令,应该使用 npm run pub
来发布应用,达到发布前的相关逻辑检测。
好了,到这里给你们介绍完一个库是如何从零开发出来的,我相信你们明白了 Ant-Design
组件的构建以及打包的整个流程,应对开发中其余一些自定义的库封装发布将会成竹在胸。
谢谢你们的阅读和鼓励,我是合一,英雄再会!