Sing with the day, dream with the night. – khalil gibran

  • Wechat official account “JavaScript Full Stack”
  • Nuggets’ Master of One ‘
  • Bilibili, The Master of One

Address of the Ant-Design warehouse

Do the front end, is not in toss is in toss on the road.

We have different solutions in different scenarios, and the development of business and common components is also different. In this paper, with the help of Ant Design, we understand how big factories define specifications, implement collaborative development schemes and control the development process when developing similar common components or class libraries. Before moving on, let’s take a look at the conventions and preparations we need to make to encapsulate such a library.

Specification implementation

Since it is a generic component or library, it cannot be separated from the following points:

  1. Development environment construction
  2. Code specification and testing
  3. Git submission
  4. packaging
  5. release

The five steps above are the core of our process for developing and publishing a component or library. Below, we dive into each step and explore the implementation principles

Development environment construction

Let’s first look at the architecture of the project

  • Component preview project generated by _site
  • Components source code
  • Dist Package the generated file
  • Docs document
  • Es file
  • Lib NPM package source
  • Site defines component preview project related files
  • Tests the test
  • Typeing type definition

Building a project to develop a UI component library has two pain points:

  1. Generate UI component library preview resources to realize the preview of the development process of the component library
  2. Compile the packaged component library code to generate the online code

In view of the above two problems, combined with our development, it can be inferred that preview projects and packaging need two different packaging compilation mechanisms, but generally only one packaging method can be used in projects, namely: Webpack configuration has only one or a set of files that distinguish the compilation environment. Bisheng and ANTD-tools. Bisheng is a framework that uses React to easily convert conforming Markdown files to generate SPA web pages. Antd-tools defines the processing of the ant-Design component library packaging.

bisheng

Bisheng’s processing process is shown below (search wechat official account: JavaScript full stack, watch video explanation)

The basic configuration

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': 0Principle:7.Principles: 7Vision:2.Visual: 2Mode:3.Patterns: 3Other:6.Other: 6.Components: 1Components:1,},typeOrder: {
      Custom: - 1.General: 0.Layout: 1.Navigation: 2.'Data Entry': 3.'Data Display': 4.Feedback: 5.Other: 6.Deprecated: 7, custom:- 1Gm:0Layout:1The navigation:2, data entry:3, data display:4The feedback:5Other:6Waste: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,
  },
};

Copy the code

This file defines how to convert a Markdown file to a preview web page according to which rules.

Once the file is defined, we just need to execute NPM start to run the preview project. To execute NPM start is to execute the following command

rimraf _site && mkdir _site && node ./scripts/generateColorLess.js && cross-env NODE_ENV=development bisheng start -c ./site/bisheng.config.js
Copy the code

antd-tools

Antd-tools is responsible for the packaging, publishing, submission guard, verification and other work of components

antd-tools run dist
antd-tools run compile
antd-tools run clean
antd-tools run pub
antd-tools run guard
Copy the code

The functions of each command will be described in detail when we go to the corresponding flow.

Code specification and testing

This project uses Typescript and component unit tests use Jest in conjunction with enzyme. Let’s take Button as an example to explain the specific use case. (Search wechat official account: JavaScript full stack, watch video explanation)

it('should change loading state instantly by default', () = > {class DefaultButton extends Component {
    state = {
      loading: false}; enterLoading =(a)= > {
    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);
});
Copy the code

Code Git commit management

I remember when I first started programming, the environment wasn’t as friendly as it is today, tools like ESLint weren’t as easy to use as they are today, and maybe a colleague would upload some code that he couldn’t read himself. What if?

In order to control the quality, we need to check whether the code is written in accordance with the agreed code style before the local Git commit. If the code fails the check, the commit is not allowed.

We use Husky to specify actions when we commit, simply by downloading Husky and configuring it in package.json

"husky": {
  "hooks": {
    "pre-commit": "pretty-quick --staged"
  }
}
Copy the code

Hooks define the hooks that we want to handle time. The intent is clear: we want to perform the specified action before commit. The code is checked with pretty- Quick.

This way, when we change the file, manage the version of the file with Git, and execute git commit, the hook will process, and pretty quick will commit if it passes, otherwise it will fail.

Packaging components

As for component packaging, a tool library — ANTD-tools was separately packaged to deal with it. We analyzed the whole process according to the information revealed to me by package.json, and related startup commands are as follows

"build": "npm run compile && npm run dist",
"compile": "antd-tools run compile",
"dist": "antd-tools run dist",
Copy the code

For the compile and dist commands, see.antd-tools.config.js in the root directory of the project

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'); }}Copy the code

We go to antD-tools and change the package to node_modules/@ant-design/tools. The processing is handed over to gulp, see gulpfile.js

// compile processing
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]);
}

// Generate package file processing
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);
  });
}
Copy the code

This completes the component packaging operation. For details, see wechat official account: JavaScript full stack

Package release

We all have a feeling, every time the package is terrified, are you fully prepared? Is it build time to build? Has the modification been confirmed? No matter how careful we are, something will go wrong, so we can define some convention rules before we release the package, and only if these rules pass can we release it. This is where we need the NPM provided hook Prepublish to handle the pre-publish operation, which is the logic defined in antD-tools. We also see gulpfile.js as seen above.

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(); }));Copy the code

The scripts definition in package.json

"prepublish": "antd-tools run guard",
"pub": "antd-tools run pub",
Copy the code

When we publish, antD-tools run Guard prevents us from publishing directly. Instead, we should use NPM run pub to publish the application and check the relevant logic before publishing.

Now that I’ve shown you how to develop a library from scratch, I’m sure you’ve understood the entire process of building and packaging ant-Design components, and will be well prepared for other custom library releases in development.

Thank you for your reading and encouragement. I am one. Farewell hero!