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