重新封裝優化React組件並打包發佈到npm私服

- 背景

  • 在前端開發時,一些常見的組件(如select)往往不能滿足現實開發需求,需要對組件進行重新的封裝和優化
  • 重新封裝優化的組件往往會用在不同的幾個項目,這個時候如果要在另外一個項目中使用這個組件,就只能把組件代碼重新copy一份,這樣存在很多弊端,第一點就是代碼冗餘;第二點是如果要對這個組件進行升級修改,這個時候得去每個項目下修改一遍,想想就頭大

爲了解決這個問題,做了以下探索,特此記錄以下操作的流程,中間有很多細節本文不詳細解釋,因爲還有很多配置不是很明白,找個機會系統講一下npm相關知識


- 爲什麼要搭建npm私有倉庫(私服)
eg:
1.有三個項目A、B 、C
2.在A項目寫了一個select組件,又不能將這個組件發佈到npm公共社區裏

沒有npm私服時的做法:

  1. 這時如果B和C項目要用帶這個組件的話,只能去A項目裏複製一份粘貼到B和C項目

  2. 如果後面select組件更新了的話,又只能複製粘貼

如上這種情況,耗費時間。有時還會忘了哪個項目的組件時最新的。一個組件都這麼麻煩,如果是多個組件和多個項目,維護簡直要瘋了。

如果我們有npm私有倉庫的做法:

  1. 將select組件發佈到私有倉庫裏
  2. 每個項目啓動前,執行npm 命令重新下載更新組件即可

私有npm私有倉庫的好處

  • 便於管理企業內的業務組件或者模塊
  • 私密性
  • 確保npm服務快速穩定(私服通常搭建在內部的服務器裏)
  • 控制npm模塊質量和安全(防止惡意代碼植入)

- 自定義組件發佈到私有倉庫的流程(Windows系統)
1.創建組件

md gldSelect
cd gldSelect

2.創建以下文件或文件夾

md build
md src

package.json

{
  "name": "gld-select",
  "version": "1.0.1",
  "description": "This is an optimized Select component",
  "main": "build/index.js",
  "peerDependencies": {
    "react-dom": "^16.12.0",
    "react": "^16.4.0"
  },
  "scripts": {
    "start": "webpack --watch",
    "build": "webpack"
  },
  "author": {
    "name": "szyy"
  },
  "license": "ISC",
  "dependencies": {
    "antd": "^3.26.5",
    "prop-types": "^15.7.2",
    "react": "^16.4.0",
    "react-dom": "^16.12.0",
    "webpack": "^4.12.0"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "babel-plugin-transform-react-jsx": "^6.24.1",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "webpack-cli": "^3.3.10"
  }
}

webpack.config.js

var path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'index.js',
    libraryTarget: 'commonjs2'
    // libraryTarget: 'umd'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),
        exclude: /(node_modules|bower_components|build)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ["env", "stage-0"]
          }
        }
      }
    ]
  },
  externals: {
    'react': 'commonjs react'
  }
};

.babelrc

{
   "presets": ["env", "stage-0"],
   "plugins": [
      "transform-object-rest-spread",
      "transform-react-jsx",
      "transform-class-properties"
   ]
}

3.將自定義組件寫入到src/index.js
src/index.js

import React from "react";
import { Select } from "antd";
import PropTypes from "prop-types";

export default class GldSelect extends Select {
  static propTypes = {
    dataSource: PropTypes.array,
    onChange: PropTypes.func,
  };

  static defaultProps = {
    dataSource: [],
  };

  init(source, isSearch) {
    this.pageNum = 1;
    this.scrollPos = 0;
    this.dataSource = source;

    this.setState({
      displayVals: this.dataSource.length > this.pageSize ?
        this.dataSource.slice(0, this.pageSize * this.pageNum) : this.dataSource,
      isSearch: isSearch,
    });
  }

  constructor(props) {
    super(props);
    this.pageSize = 20;
    this.pageNum = 1;
    this.scrollPos = 0;
    this.dataSource = props.dataSource;
    this.state = {
      displayVals: this.dataSource.length > this.pageSize ?
        this.dataSource.slice(0, this.pageSize * this.pageNum) : this.dataSource,
      isSearch: false,
    };
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    const { dataSource } = this.props;
    const ds = [].concat(dataSource);
    const preDs = [].concat(prevProps.dataSource);
    if (preDs.sort().toString() !== ds.sort().toString()) {
      this.init(dataSource)
    }
  }

  handleChange = v => {
    const { onChange, dataSource } = this.props;
    const { isSearch } = this.state;
    onChange && onChange(v);
    /*點擊事件分爲兩種情況:
    * 1.一般的下拉框選中,這時只需要執行props中的onChange
    * 2.根據關鍵字搜索後,點擊選中時,需要恢復到初始的狀態
    * */
    if (isSearch === true) {
      this.init(dataSource)
    }
  };

  handleSearch = v => {
    v = v || "";
    const filterWord = v.trim();
    const { dataSource } = this.props;
    const newSource = dataSource.filter(item =>
      item.includes(filterWord)
    );
    this.init(newSource, true);
  };

  handlePopupScroll = e => {
    const { displayVals } = this.state;
    e.persist();
    const { target } = e;
    const st = target.scrollTop;
    if (st === 0 && this.scrollPos) {
      target.scrollTop = this.scrollPos;
    }
    if (
      st + target.offsetHeight + 2 >= target.scrollHeight &&
      displayVals.length < this.dataSource.length
    ) {
      this.pageNum += 1;
      this.setState({ displayVals: this.dataSource.slice(0, this.pageSize * this.pageNum) });
      this.scrollPos = st;
    } else {
      this.scrollPos = 0;
    }
  };

  render() {
    const { dataSource, ...rest } = this.props;
    const { displayVals } = this.state;
    return (
      <Select
        {...rest}
        onChange={this.handleChange}
        onPopupScroll={this.handlePopupScroll}
        dropdownMatchSelectWidth={false}
        maxTagCount={5}
        onSearch={this.handleSearch}
      >
        {displayVals.map((opt) => (
          <Select.Option key={opt} value={opt}>
            {opt}
          </Select.Option>
        ))}
      </Select>
    );
  }
}

4.安裝相關npm包並打包

npm i
npm run build
npm link

npm link將用於在我們開發測試項目時對我們的組件進行開發測試。

5.測試組件是否可用,首先創建一個測試項目

npm i -g create-react-app
md gld-test
cd gld-test
create-react-app .
npm link gldSelect

測試項目創建成功後,在src/App.js中添加自定義組件gldSelect,並完成測試

import GldSelect from 'gldSelect'; 
 ...
 <GldSelect />

App.js

import React from 'react';
import logo from './logo.svg';
import './App.css';
import GldSelect from 'gldSelect';
import {
  Button,
  Row,
  Col,
  Card,
  Form,
  Select,
  DatePicker,
  Icon,
  Tabs,
  notification,
  Table,
} from 'antd';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
      <div>
        <Form>
          <Row>
            <Col span={9}>
              <Form.Item>
                <GldSelect
                  style={{ width: '100%' }}
                  placeholder="選擇預算版本號"
                  mode="multiple"
                  dataSource={[1,3,4,5,7,8,9,0]}
                />
              </Form.Item>
            </Col>
          </Row>
        </Form>
      </div>
    </div>
  );
}

export default App;

添加完成後,執行

npm start

如果在測試項目中可用,則該自定義組件可用,可上傳至npm私有庫

6.將自定義組件上傳私有庫

cd gldSelect

安裝nrm

npm install -g nrm

nrm(npm registry manager )是npm的鏡像源管理工具,有時候國外資源太慢,使用這個就可以快速地在 npm 源間切換

添加私有庫鏡像源

nrm add szyy http://xx.xxx.x.xxx:xxxx/repository/npm-host/

切換到私有庫鏡像源

nrm use szyy

登錄私有庫鏡像源賬號

npm login

(按照要求輸入賬號密碼以及郵箱)

登錄成功後,執行以下命令完成組件上傳

npm publish

7.項目中使用自定義組件
通過npm的config命令配置指向國內私有鏡像源:以後所有從倉庫獲取的包都從這個地址獲取,不走國外的地址

npm config set registry http://xx.xxx.x.xxx:xxxx/repository/npm/

在項目中執行以下命令,安裝自定義組件

npm install gld-select

在js文件代碼中直接引用

import GldSelect from 'gld-select'

- 參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章