react 구성 요소 라이브러리 구축 기록

42424 단어

1. 프로젝트 초기화

  • 초기화 항목
  • git init
    npm init -y
    
  • 생성.gitignore
  • node_modules
    coverage
    dist
    es
    lib
    
    package-lock.json
    

    2. storybook 기반 개발 테스트 환경 구축


    2.1 프로젝트 신속 구축

  • 참조 문서
  • storybook react 환경 신속 구축
  • npx -p @storybook/cli sb init --type react
    
  • 현재 카탈로그 소개
  • stories: storybook 메인 디렉터리는 일반 항목의 src 디렉터리 index에 해당합니다.stories.js:storybook의 입구 파일.storybook:storybook 구성 디렉토리
    ├── .gitignore
    ├── package.json
    ├── package-lock.json
    ├── stories
    │   ├── index.stories.js
    └── .storybook
        ├── addons.js
        └── config.js
    

    2.2 디렉터리 구조 조정


    stories/pages 디렉터리,storybook의 모든 관련 페이지를 저장하는 데 사용되며,stories/config.js는 페이지로 입구 파일 index에 설정됩니다.stories.js에서 인용하고 이 설정에 따라 모든 관련 페이지를 렌더링합니다. 또한 우리storybook에 1급 디렉터리 가 있고 디렉터리에 페이지가 있다고 가정하면 그에 대응하는stories/pages/base/Introduce 디렉터리를 추가하고 디렉터리에api를 추가합니다.md는 대응하는 구성 요소api 문서를 작성하는 데 사용됩니다. (물론 소개 페이지에api 문서가 없습니다.) index.css는 현재 페이지의 스타일 파일로 index.jsx는 현재 페이지의 입구 파일이고subpage는 페이지의 하위 페이지를 저장하는 데 사용됩니다.
    ├── .gitignore
    ├── package.json
    ├── package-lock.json
    ├── stories
    │   ├── config.js
    │   ├── index.stories.js
    │   └── pages
    │       └── base
    │           └── Introduce
    │               ├── api.md
    │               ├── index.css
    │               ├── index.jsx
    │               └── subpage
    └── .storybook
        ├── addons.js
        └── config.js
    

    2.3 테스트 코드 작성

  • stories/pages/base/Introduce/index.jsx
  • import React from 'react';
    import './index.css';
    export default () => {
      return (
        <div>
          react     
        div>
      );
    };
    
    
  • stories/config.js
  • import Introduce from './pages/base/Introduce';
    export default [
      {
        title: '  ',
        module: '  ',
        component: Introduce
      }
    ];
    
  • stories/index를 작성합니다.stories.js
  • import React from 'react';
    import { storiesOf } from '@storybook/react';
    import config from './config';
    
    config.forEach( v => (storiesOf(v.module, module).add(v.title, v.component)));
    
  • npm 스크립트 추가
  • {
      "scripts": {
    +  "start": "npm run storybook",
        "storybook": "start-storybook -p 8080",
        "build-storybook": "build-storybook"
      },
    }
    
  • 실행 스크립트npm start
  • 2.4 웹 패키지 구성 사용자 정의


    storybook은 자신의 웹 패키지 설정이 있지만 복잡한 상황을 충족시킬 수 없습니다. storybook에서 웹 패키지 설정을 만드는 .storybook/webpack.config.js를 통해 정의할 수 있습니다.
  • 다운로드 관련 의존도
  • # 1. webpack   
    npm install webpack webpack-cli -D
    
    # 2. babel-loader        
    npm install babel-loader @babel/core -D
    
    #  3. babel          
    npm install @babel/preset-env @babel/preset-react -D
    
    #  4. babel          
    npm install @babel/plugin-transform-runtime -D
    npm install @babel/plugin-proposal-decorators -D
    npm install @babel/plugin-transform-async-to-generator -D
    
    # 5. eslint-loader     
    npm install eslint eslint-loader -D
    
    # 6. eslint       
    npm install babel-eslint eslint-plugin-babel eslint-plugin-react -D
    
    # 7.             
    npm install style-loader css-loader sass-loader node-sass postcss-loader -D
    
    # 8. postcss-loader          
    npm install autoprefixer -D
    
    # 9.           
    npm install url-loader file-loader -D
    
    # 10.           
    npm install raw-loader -D
    
  • 생성.storybook/webpack.config.js 파일
  • const path = require('path');
    const webpack = require('webpack');
    
    //     
    const alias = {};
    
    module.exports = {
      mode: 'production',
      module: {
        rules: [
          { // js     
            test: /\.(mjs|js|jsx)$/,
            exclude: [ path.resolve(__dirname, 'node_modules') ],
            use: ['babel-loader', 'eslint-loader']
          }, { //       
            test: /\.(css|scss)$/,
            use: [
              'style-loader', {
                loader: 'css-loader',
                options: {
                  sourceMap: false,
                }
              }, {
                loader: 'postcss-loader',
                options: { javascriptEnabled: true, sourceMap: false },
              }, {
                loader: 'sass-loader'
              }
            ],
          }, { //       
            test: /\.(png|jpg|gif|woff|svg|eot|ttf)$/,
            use: [{
              loader: 'url-loader',
              options: {
                limit: 10 * 1000,
              }
            }]
          }, { //       (         markdown   )
            test: /\.(txt|md)$/,
            use: 'raw-loader',
          },
        ]
      },
    
      plugins: [
        new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn|en-gb/),
      ],
    
      //     
      resolve: {
        alias,
        //          
        extensions: ['.mjs', '.js', '.jsx'],
      },
    }
    
  • 프로젝트에 새로운babel 프로필.babelrc
  • {
      "plugins": [
        //  api         ,        api
        ["@babel/plugin-transform-runtime"],
        //    
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        // asyn await   
        ["@babel/plugin-transform-async-to-generator"]
      ],
      "presets": ["@babel/preset-react", "@babel/preset-env"]
    }
    
  • 프로젝트에 추가된postcss 프로필postcss.config.js
  • module.exports = {
      plugins: [
        require("autoprefixer")({
          browsers: [
            "last 2 versions",
            "Android >= 4.4",
            "Firefox ESR",
            "not ie < 9",
            "ff >= 30",
            "chrome >= 34",
            "safari >= 6",
            "opera >= 12.1",
            "ios >= 6"
          ]
        })
      ]
    };
    
  • 프로젝트에 새로운eslint 프로필.eslintrc.js.eslintignore
  • // .eslintrc.js     
    module.exports = {
      parserOptions: {
        ecmaVersion: 8,
        sourceType: "module",
        ecmaFeatures: {
          jsx: true
        }
      },
      parser: "babel-eslint",
      plugins: ["babel", "react"],
      extends: "eslint:recommended",
      env: {
        es6: true,
        browser: true,
        commonjs: true
      },
      globals: {
        process: true,
        describe: true,
        it: true,
        __dirname: true,
        expect: true,
        jest: true,
        beforeAll: true,
        afterEach: true
      },
      rules: {
        "object-shorthand": "error",
        "generator-star-spacing": ["error", "after"],
        camelcase: ["error", { properties: "never" }],
        eqeqeq: ["error", "smart"],
        "linebreak-style": ["error", "unix"],
        "new-cap": "error",
        "no-array-constructor": "error",
        "no-lonely-if": "error",
        "no-loop-func": "error",
        "no-param-reassign": "error",
        "no-sequences": "error",
        "no-shadow-restricted-names": "error",
        "no-unneeded-ternary": "error",
        "no-unused-expressions": "error",
        "no-unused-vars": "off",
        "no-use-before-define": ["error", "nofunc"],
        "no-var": "error",
        "prefer-arrow-callback": "error",
        "prefer-spread": "error",
        "prefer-template": "error",
        "wrap-iife": ["error", "inside"],
        yoda: ["error", "never"],
        "react/jsx-uses-react": "error",
        "react/jsx-uses-vars": "error",
        "react/jsx-no-undef": ["error", { allowGlobals: true }],
        "react/jsx-no-bind": ["error", { allowArrowFunctions: true }],
        "react/jsx-key": "error",
        "react/no-unknown-property": "error",
        "react/no-string-refs": "error",
        "react/no-direct-mutation-state": "error",
        "no-console": "off"
      }
    };
    
    # .eslintignore     
    tests
    node_modules
    *.bundle.js
    *.js.map
    .history
    dist
    .vscode
    **/*.snap
    

    3. 테스트 구성 요소의 생성과 발표


    3.1 components 디렉터리 디자인


    프로젝트 루트 디렉터리에components 디렉터리를 만들어서 구성 요소 라이브러리의 모든 구성 요소를 저장합니다.components 디렉터리의 구조는antd를 참고하여 설계합니다. 구성 요소 input-number가 있다고 가정하면components는 다음과 같습니다.
  • index.js를 구성 요소 라이브러리의 입구 파일로 사용하기
  • assets/iconfont 글꼴 아이콘 파일 저장
  • assets/style 일반 스타일 파일을 저장하는 데 사용
  • input-number는 input-number 구성 요소의 모든 원본을 저장하는 데 사용됩니다
  • input-number/index.jsx를 구성 요소의 입구 파일로 사용하기
  • input-number/style 구성 요소를 저장하는 데 사용되는 모든 스타일 파일
  • input-number/style/index.js 구성 요소 스타일의 입구 파일 (이 구성 요소는 구성 요소에 필요한 모든 스타일 파일을 도입하고babel-plugin-import와 함께 필요에 따라 불러오는 기능을 수행합니다)
  • __tests__ 구성 요소를 저장하는 단위 테스트
  • ├── components
    │   ├── assets
    │   │   ├── iconfont
    │   │   └── style
    │   ├── index.js
    │   └── input-number
    │       ├── index.jsx
    │       ├── style
    │       │   ├── index.js
    │       │   └── index.scss
    │       └── __tests__
    ....
    

    3.2 테스트 구성 요소 input-number 작성

  • 작성components/input-number/index.jsx
  • import React from 'react';
    
    export default () => {
      return (
        <div className="qyrc-input-num">
              : input-number
        div>
      );
    };
    
    
  • 작성components/input-number/style/index.scss
  • .qyrc-input-num {
      color: #999;
    }
    
  • 작성components/input-number/style/index.js
  • //                    
    import './index.scss';
    
  • 컴파일components/index.js 내보내기 구성 요소
  • export { default as InputNumber } from './input-number';
    
  • storybook에서 구성 요소 테스트
  • stories/pages/base/Introduceindex.jsx는 InputNumber 구성 요소를 참조하고 프로젝트를 실행하여 구성 요소를 테스트합니다.
    import React from 'react';
    import './index.css';
    import './index.scss';
    //    InputNumber      
    import { InputNumber } from '../../../../components';
    import '../../../../components/input-number/style/index';
    
    export default () => {
      return (
        <div>
          react     
          <InputNumber />
        div>
      );
    };
    

    3.3 구성 스크립트에서 구성 요소 컴파일 패키지


    npm 패키지는 발표할 때 발표할 파일을 간단하게 컴파일하고 포장해야 합니다. 저희 모듈 라이브러리는 항상 모듈 라이브러리를 ES 모듈과CommonJS, UMD 모듈로 컴파일해야 합니다.
    ES 모듈과 CommonJS 모듈의 컴파일 방식은 대동소이하다. 모두 babel을 통해 구성 요소 라이브러리의 js 모듈을 컴파일하고 다른 파일은 간단하게 복사하기만 한다. 물론 스타일 파일에 대해 css 파일을 추가로 컴파일해야 한다. 유일한 차이점은 포장된 js 모듈과 다르다는 것이다. 하나는 ES 모듈이고 하나는CommonJS 모듈이다.컴파일된 ES 모듈과 CommonJS 모듈의 디렉터리 구조는 babel-plugin-import 원리를 충족시켜 수요에 따라 불러오는 기능을 실현해야 한다.
    UMD 모듈은 웹 패키지를 통해component의 입구 파일을 완전하게 패키지 컴파일합니다

    3.3.1 babel을 통해 구성 요소 라이브러리의 js 모듈을 컴파일하기

  • 설치에 필요한 의존도
  • npm install cross-env @babel/cli -D
    
  • npm 스크립트 작성: 구성 요소 라이브러리의 js 모듈을 ES 모듈과CommonJS 모듈로 컴파일
  • {
      "scripts": {
    +   "build:lib": "cross-env OUTPUT_MODULE=commonjs babel components -d lib --ignore **/__tests__",
    +   "build:es": "babel components -d es --ignore **/__tests__"
      }
    }
    

    3.3.2gulp를 통해 구성 요소 라이브러리 처리

  • 설치에 필요한 의존도
  • npm install gulp -D
    npm install gulp-sass -D
    npm install gulp-concat -D
    npm install gulp-autoprefixer -D
    npm install gulp-cssnano -D
    npm install gulp-filesize -D
    npm install gulp-sourcemaps -D
    npm install gulp-rename -D
    npm install gulp-replace -D
    
  • scripts/gulpfile을 만듭니다.js
  • /**
     * @name gulpfile.js
     * @description     css  
     * @description    cuke-ui
     */
    
    const fs = require('fs');
    const path = require('path');
    const gulp = require('gulp');
    const concat = require('gulp-concat');
    const sass = require('gulp-sass');
    const autoprefixer = require('gulp-autoprefixer');
    const cssnano = require('gulp-cssnano');
    const size = require('gulp-filesize');
    const sourcemaps = require('gulp-sourcemaps');
    const rename = require('gulp-rename');
    const replace = require('gulp-replace');
    const { name } = require('../package.json');
    
    const browserList = [
      'last 2 versions',
      'Android >= 4.0',
      'Firefox ESR',
      'not ie < 9'
    ];
    
    const DIR = {
      //     
      scss: path.resolve(__dirname, '../components/**/*.scss'),
      buildSrc: path.resolve(__dirname, '../components/**/style/*.scss'),
      style: path.resolve(__dirname, '../components/**/style/index.js'),
      
      //     
      lib: path.resolve(__dirname, '../lib'),
      es: path.resolve(__dirname, '../es'),
      dist: path.resolve(__dirname, '../dist')
    };
    
    //    scss   
    gulp.task('copyScss', () => {
      return gulp
        .src(DIR.scss)
        .pipe(gulp.dest(DIR.lib))
        .pipe(gulp.dest(DIR.es));
    });
    
    //   scss        
    gulp.task('copyCss', () => {
      return gulp
        .src(DIR.scss)
        .pipe(sourcemaps.init())
        .pipe(sass())
        .pipe(autoprefixer({ browsers: browserList }))
        .pipe(size())
        .pipe(cssnano())
        .pipe(gulp.dest(DIR.lib))
        .pipe(gulp.dest(DIR.es));
    });
    
    //    style/css.js
    gulp.task('createCss', () => {
      return gulp
        .src(DIR.style)
        .pipe(replace(/\.scss/, '.css'))
        .pipe(rename({ basename: 'css' }))
        .pipe(gulp.dest(DIR.lib))
        .pipe(gulp.dest(DIR.es));
    });
    
    //              dis   
    gulp.task('dist', () => {
      return gulp
        .src(DIR.buildSrc)
        .pipe(sourcemaps.init())
        .pipe(sass())
        .pipe(autoprefixer({ browsers: browserList }))
        .pipe(concat(`${name}.css`))
        .pipe(size())
        .pipe(gulp.dest(DIR.dist))
        .pipe(sourcemaps.write())
        .pipe(rename(`${name}.css.map`))
        .pipe(size())
        .pipe(gulp.dest(DIR.dist))
        .pipe(cssnano())
        .pipe(concat(`${name}.min.css`))
        .pipe(size())
        .pipe(gulp.dest(DIR.dist))
        .pipe(sourcemaps.write())
        .pipe(rename(`${name}.min.css.map`))
        .pipe(size())
        .pipe(gulp.dest(DIR.dist));
    });
    
    gulp.task('default', gulp.parallel(
      'dist',
      'copyCss',
      'copyScss',
      'createCss',
    ));
    
  • npm 스크립트 추가
  • {
      "scripts": {
    +   "build:css": "cd scripts && gulp",
        "build:lib": "cross-env OUTPUT_MODULE=commonjs babel components -d lib --ignore **/__tests__",
        "build:es": "babel components -d es --ignore **/__tests__"
      }
    }
    

    3.3.3 웹팩을 통한 어셈블리 라이브러리 UMD 모듈 패키지

  • 관련 종속 패키지 설치
  • npm install uglifyjs-webpack-plugin -D
    npm install optimize-css-assets-webpack-plugin -D
    npm install mini-css-extract-plugin -D
    npm install progress-bar-webpack-plugin -D
    
  • scripts/build을 만듭니다.umd.js
  • /**
     * @name UMD      
     * @description    cuke-ui
     * @description      [dist]
     * CMD Node.js   
     * AMD      
     * UMD          
     */
    
    const fs = require("fs");
    const path = require("path");
    const webpack = require("webpack");
    const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
    const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const ProgressBarPlugin = require('progress-bar-webpack-plugin');
    
    const { version, name, description } = require("../package.json");
    
    const LOGO = `
                  __                    _
      _______  __/ /_____        __  __(_)
     / ___/ / / / //_/ _ \\______/ / / / /
    / /__/ /_/ / ,< /  __/_____/ /_/ / /  
    \\___/\\__,_/_/|_|\\___/     \\__,_/_/
    
    `
    
    const config = {
      mode: "production",
      entry: {
        [name]: ["./components/index.js"]
      },
    
      //umd     
      output: {
        library: name,
        libraryTarget: "umd",
        umdNamedDefine: true, //           AMD        
        path: path.join(process.cwd(), "dist"),
        filename: "[name].min.js"
      },
      //react   react-dom    
      externals: {
        react: {
          root: "React",
          commonjs2: "react",
          commonjs: "react",
          amd: "react"
        },
        "react-dom": {
          root: "ReactDOM",
          commonjs2: "react-dom",
          commonjs: "react-dom",
          amd: "react-dom"
        }
      },
      resolve: {
        enforceExtension: false,
        extensions: [".js", ".jsx", ".json", ".less", ".css"]
      },
      module: {
        rules: [
          {
            test: /\.js[x]?$/,
            use: [
              {
                loader: "babel-loader"
              }
            ],
            exclude: "/node_modules/",
            include: [path.resolve("components")]
          },
          {
            test: /\.(le|c)ss$/,
            use: [
              MiniCssExtractPlugin.loader,
              "css-loader",
              { loader: "postcss-loader", options: { sourceMap: false } },
              {
                loader: "sass-loader",
                options: {
                  sourceMap: false
                }
              }
            ]
          },
          {
            test: /\.(jpg|jpeg|png|gif|cur|ico)$/,
            use: [
              {
                loader: "file-loader",
                options: {
                  name: "images/[name][hash:8].[ext]" //          images       .     
                }
              }
            ]
          }
        ]
      },
      optimization: {
        minimizer: [
          new UglifyJsPlugin({
            cache: true,
            parallel: true,
            uglifyOptions: {
              compress: {
                drop_debugger: true,
                drop_console: false
              },
            }
          }),
          new OptimizeCSSAssetsPlugin({
            //   css    ExtractTextPlugin     
            cssProcessor: require("cssnano"),
            cssProcessorOptions: { discardComments: { removeAll: true } }, //       
            canPrint: true //           
          })
        ],
        noEmitOnErrors: true,
      },
      plugins: [
        new ProgressBarPlugin(),
        new MiniCssExtractPlugin({
          filename: "[name].min.css"
        }),
        //                
        new webpack.BannerPlugin(` 
    ${name} v${version}
    ${description}
    ${LOGO}
    `
    ), new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production"), __DEBUG__: false, }), new webpack.LoaderOptionsPlugin({ minimize: true }), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ] }; module.exports = config;
  • npm 스크립트 추가
  • {
      "scripts": {
    +   "build:publish": "npm run build:lib && npm run build:es && npm run build:css && npm run build:umd",
    +   "build:umd": "webpack --config ./scripts/build.umd.js",
        "build:css": "cd scripts && gulp",
        "build:lib": "cross-env OUTPUT_MODULE=commonjs babel components -d lib --ignore **/__tests__",
        "build:es": "babel components -d es --ignore **/__tests__"
      }
    }
    

    3.4 패키지 수정json

  • private 설정 항목이 개인 패키지인지 여부
  • files는 패키지를 발표할 때 발표할 파일과 디렉터리를 설정합니다
  • main 설정 패키지의 입구 파일
  • module 설정 npm 패키지의 모듈 입구
  • peerDependencies 설정 npm 패키지 동등 의존 패키지
  • {
    + "private": false,
    + "files": [
    +   "lib",
    +   "es",
    +   "dist",
    +   "LICENSE"
    + ],
    + "main": "lib/index.js",
    + "module": "es/index.js",
    + "peerDependencies": {
    +   "react": ">=16.8.0",
    +   "react-dom": ">=16.8.0"
    + },
    }
    

    3.4 어셈블리 게시

  • 구성 요소 라이브러리 컴파일
  • npm run build:publish
    
  • 구성 요소 발표
  • # 1.       
    npm config set registry http://registry.npmjs.org
    
    # 2.    npm
    npm login
    
    # 3.    
    npm publish --access public
    
    # 4.            
    npm config set registry https://registry.npm.taobao.org/
    

    4. 기타 구성


    4.1 commit 검사 및 릴리즈 구성

  • 종속 패키지 설치
  • # husky    
    npm install husky --save-dev
    
    # commitlint      
    npm install @commitlint/config-angular @commitlint/cli --save-dev
    
    # commitizen    
    npm install commitizen --save-dev
    npm install commitizen -g
    
    # standard-version    
    npm install standard-version --save-dev
    
  • commitlint와commitizen 설정
  • #    commitlint     
    echo "module.exports = {extends: ['@commitlint/config-angular']};" > commitlint.config.js
    # commitizen    
    commitizen init cz-conventional-changelog --save-dev --save-exact
    
  • 패키지 업데이트.json
  • {
      "scripts": {
    +   "commit": "git-cz",
    +   "release": "standard-version"
      },
    + "husky": {
    +   "hooks": {
    +     "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    +   }
    + }
    }
    

    4.2 editorconfig 구성

  • 신규 .editorconfig 구성 파일
  • # http://editorconfig.org
    root = true
    
    [*]
    indent_style = space
    indent_size = 2
    end_of_line = lf
    charset = utf-8
    trim_trailing_whitespace = true
    insert_final_newline = true
    
    [*.md]
    trim_trailing_whitespace = false
    
    [Makefile]
    indent_style = tab
    

    좋은 웹페이지 즐겨찾기