和白晝一塊兒歌唱,和黑夜一塊兒作夢。——紀伯倫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
組件的構建以及打包的整個流程,應對開發中其餘一些自定義的庫封裝發佈將會成竹在胸。
謝謝你們的閱讀和鼓勵,我是合一,英雄再會!