Ant Design從無到有,帶你體悟大廠前端開發範式

和白晝一塊兒歌唱,和黑夜一塊兒作夢。——紀伯倫javascript

  • 微信公衆號 《JavaScript全棧
  • 掘金 《合一大師
  • Bilibili 《合一大師

Ant-Design倉庫地址css

作前端,不是在折騰就是在折騰的路上。html

不一樣的場景咱們有不一樣的應對方案,業務和通用組件的開發也有所差別,這篇文章藉助Ant Design,一塊兒體悟大廠在開發相似通用組件或類庫時,如何定義規範,如何實施協同開發方案,怎麼把控開發過程等。到正文前,先來看看咱們封裝這樣一個庫前須要作那些約定和準備。前端

規範實施

既然是通用組件或者庫,就離不開一下幾點:java

  1. 開發環境構建
  2. 代碼規範與測試
  3. 代碼git提交
  4. 打包
  5. 發佈

以上五個步驟是咱們開發併發布組件或庫的核心流程,如下,咱們深刻到每個步驟,深究實現原理node

開發環境構建

咱們先看一下項目的架構react

  • _site 生成的組件預覽項目
  • components 組件源碼
  • dist 打包生成的文件
  • docs 文檔
  • es 類型文件
  • lib npm包源碼
  • site 定義組件預覽項目相關文件
  • tests 測試
  • typeing 類型定義

開發UI組件庫的項目構建有以下兩個痛點:webpack

  1. 生成UI組件庫預覽資源,實現組件庫開發過程的預覽
  2. 編譯打包組件庫代碼,生成線上代碼

看到以上兩個問題,結合咱們開發,能夠推測出預覽項目和打包須要兩套不一樣打包編譯機制,可是在項目中通常只能使用一種打包方式,即:webpack配置只有一個或一套區分編譯環境的文件。因此咱們考慮這兩種場景下使用兩種不一樣方式進行打包處理,最終咱們選用的方案是:bishengantd-tools,這裏作一個解釋,bisheng 是一個使用React輕鬆將符合約定的Markdown文件經過轉換生成SPA網頁的框架;antd-tools 定義了ant-design組件庫打包相關的處理方案。git

bisheng

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負責組件的打包、發佈、提交守衛、校驗等工做

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); }); 複製代碼

代碼Git提交管理

記得我剛入門編程那會兒,大環境生態尚未如今友好,相似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",
複製代碼

compiledist 命令配置見項目根路徑下 .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 publishantd-tools run guard 執行,阻止咱們直接使用發佈命令,應該使用 npm run pub 來發布應用,達到發佈前的相關邏輯檢測。

好了,到這裏給你們介紹完一個庫是如何從零開發出來的,我相信你們明白了 Ant-Design 組件的構建以及打包的整個流程,應對開發中其餘一些自定義的庫封裝發佈將會成竹在胸。

謝謝你們的閱讀和鼓勵,我是合一,英雄再會!

相關文章
相關標籤/搜索