建立公司內部 Private NPM — Verdaccio — 實作 React Styled Components Library 安裝、打包、上傳與下載

Angel
Its Ok to Make Mistakes
17 min readJun 4, 2019

Private NPM 提供建立一套公司內部專屬 React Components layout library 的可能。Verdaccio 是一套輕量的 Private NPM proxy registry,當其他 Developer 要 import 各種客製化元件,只要在安裝後更新到最新的版本,即可無痛翻新整個 Layout。

安裝流程Vardaccio 安裝、設定 registry、增加使用者

打包流程:排除不需要打包的部分、設定打包工具、svgr 轉 svg、搬移圖檔

上傳與下載流程:將 Library publish、 檔案太大時的設定

安裝流程

$ npm install --global verdaccio
  • 設訂 Registry 到 localhost:4873
$ npm set registry http://localhost:4873/
  • 可以在根目錄中的 .npmrc 裡面看到 registry=://localhost:4873/
  • 此時 run verdaccio 後打開 http://localhost:4873/ 就可以看到在 local 能 login 的畫面
$ verdaccio
  • 增加使用者 > 輸入帳號、密碼和 email
$ npm adduser --registry  http://localhost:4873

打包流程

打開要包裝成 Library 的 React Layout 專案後

  • 增加一個 .npmignore 檔案把不需要打包進 Library 的東西「排除」
node_modules/
public
Reference
src
  • .gitignore 檔案中加上 Library 才不會一起 commit 進 Git
lib
.tmp
  • 增加一個 .babelrc.js 檔案設定打包工具
module.exports = {
env: {
es: {
presets: [
['@babel/preset-env', { modules: false }],
'@babel/preset-react'
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-do-expressions',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-function-bind',
'@babel/plugin-proposal-logical-assignment-operators',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-chaining',
['@babel/plugin-proposal-pipeline-operator', { proposal: 'minimal' }],
'@babel/plugin-proposal-throw-expressions',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-runtime',
[
'transform-rename-import',
{
original: '(.*)/([^/]+)\\.svg$',
replacement: (text, $1, $2) => {
return (
$1 + '/' + $2.replace(/(^|_)./g, s => s.slice(-1).toUpperCase())
)
}
}
]
]
}
}
}
  • 增加 svgrTemplate.js 檔案 (package.json file 會解釋)
const types = require('@babel/types')function template(babelAPI, opts, values) {
const componentName = types.identifier('ReactComponent')
const exports = types.exportNamedDeclaration(null, [
types.exportSpecifier(componentName, componentName)
])
return babelAPI.template.ast`
${values.imports}
const ${componentName} = (${values.props}) => ${values.jsx}
${exports}
`
}
module.exports = template
  • 在 package.json 將此版本設訂為第一版
"version": "0.1.0",
  • 加上 lib 作為打包後輸出路徑
"main": "lib",
"module": "lib",
  • 移除 package.json 上的 "private": true, 讓專案變成可共用的
  • 只留下不會與使用 Library 的 Project 衝突的 dependencies (避免影響安裝 project 的 dependency 衝突,假設兩邊都基於 React,但只要以使用 Library Project 的為主即可)
"dependencies": {
"react-swipe": "5.1.1",
"styled-components": "^4.2.0",
"swipe-js-iso": "2.0.0"
}
  • 其餘打包工具和不需要一起打包進 Library 裡放在 devDependencies
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/plugin-proposal-class-properties": "7.2.3",
"@babel/plugin-proposal-decorators": "7.2.3",
"@babel/plugin-proposal-do-expressions": "7.0.0",
"@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.2.0",
"@babel/plugin-proposal-function-bind": "7.0.0",
"@babel/plugin-proposal-logical-assignment-operators": "7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.0.0",
"@babel/plugin-proposal-optional-chaining": "7.0.0",
"@babel/plugin-proposal-pipeline-operator": "7.3.2",
"@babel/plugin-proposal-throw-expressions": "7.0.0",
"@babel/plugin-syntax-dynamic-import": "7.0.0",
"@babel/plugin-transform-runtime": "7.2.0",
"@babel/preset-env": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "7.4.2",
"@babel/types": "^7.4.4",
"@svgr/cli": "^4.2.0",
"babel-plugin-transform-rename-import": "^2.3.0",
"cross-env": "^5.2.0",
"node-sass": "^4.11.0",
"prettier": "^1.17.0",
"prop-types": "^15.7.2",
"query-string": "^6.4.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.0",
"rimraf": "^2.6.3"
},

其中,@svgr/cli 用來處理 svg 打包,而 svg 打包後會從 svg 轉為 React Components ,使用套件 babel-plugin-transform-rename-import 將匯入的 SVG 自動轉符合 import 的格式,svgr 自動把檔案轉為駝峰命名。

cross-env 處理跨平台設置 NODE_ENV 指令

rimraf 處理跨平台刪除指令

  • 在 scripts 設定 build:lib 指令時所需要打包的東西
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"format": "prettier --single-quote --no-semi --write \"src/**/*.{js,jsx,scss}\"",
"test": "react-scripts test",
"eject": "react-scripts eject",
"pre-clean": "rimraf lib",
"post-clean": "rimraf .tmp",
"build-js": "cross-env NODE_ENV=es babel src .tmp --out-dir lib",
"build-svg": "svgr src --template svgrTemplate.js --out-dir .tmp",
"cp:resources": "cp -R src/img lib",
"build:lib": "npm run pre-clean && npm run build-svg && npm run build-js && npm run post-clean && npm run cp:resources"
},
  • 其中,pre-clean 先清除上次打包出來的 lib 資料夾,然後開始轉 SVG,使用 svgr 打包後所有的 .svg 檔案會變 .js 檔案。
  • 由於我們使用 import { ReactComponent as MyComponent } form './mySvg.svg' 的語法引用 SVG,因此要使用 svgrTemplate.js 去改掉所有 .svg 轉出來的 .js 檔案來符合 import 的格式。
  • SVG 轉成 js 會產生 .tmp 資料夾,使用 Babel 把 src 與 .tmp 一起轉譯輸出到 lib 資料夾,並且修改引用 SVG 的路徑 (包含修改為駝峰命名),從 import { ReactComponent as MyComponent } from '../my_space.svg' 變成 import { ReactComponent as MyComponent } from '../MySpace'
  • 輸出完成後刪掉 .tmp , 接著才把 img 資料夾圖片資源複製到 lib 資料夾當中,讓 image 找得到。

以下為完整的 package.json

{
"name": "myLibrary",
"version": "0.1.0",
"main": "lib",
"module": "lib",
"dependencies": {
"react-swipe": "5.1.1",
"styled-components": "^4.2.0",
"swipe-js-iso": "2.0.0"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/plugin-proposal-class-properties": "7.2.3",
"@babel/plugin-proposal-decorators": "7.2.3",
"@babel/plugin-proposal-do-expressions": "7.0.0",
"@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.2.0",
"@babel/plugin-proposal-function-bind": "7.0.0",
"@babel/plugin-proposal-logical-assignment-operators": "7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.0.0",
"@babel/plugin-proposal-optional-chaining": "7.0.0",
"@babel/plugin-proposal-pipeline-operator": "7.3.2",
"@babel/plugin-proposal-throw-expressions": "7.0.0",
"@babel/plugin-syntax-dynamic-import": "7.0.0",
"@babel/plugin-transform-runtime": "7.2.0",
"@babel/preset-env": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "7.4.2",
"@babel/types": "^7.4.4",
"@svgr/cli": "^4.2.0",
"babel-plugin-transform-rename-import": "^2.3.0",
"cross-env": "^5.2.0",
"mathjs": "^5.9.0",
"node-sass": "^4.11.0",
"prettier": "^1.17.0",
"prop-types": "^15.7.2",
"query-string": "^6.4.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.0",
"rimraf": "^2.6.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"format": "prettier --single-quote --no-semi --write \"src/**/*.{js,jsx,scss}\"",
"test": "react-scripts test",
"eject": "react-scripts eject",
"pre-clean": "rimraf lib",
"post-clean": "rimraf .tmp",
"build-js": "cross-env NODE_ENV=es babel src .tmp --out-dir lib",
"build-svg": "svgr src --template svgrTemplate.js --out-dir .tmp",
"cp:resources": "cp -R src/img lib",
"build:lib": "npm run pre-clean && npm run build-svg && npm run build-js && npm run post-clean && npm run cp:resources"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
  • 執行 yarn 重新安裝一次 package.json
$ yarn
  • 更改 img 路徑從 (因為打包沒辦法 import 資料夾的 index,所以要改為直接 import index 檔案)

../img -> ../img/index

../../img -> ../../img/index

  • 開始 build lib 資料夾
$ yarn run build:lib

完成後後準備上傳第一版的 Library

上傳與下載流程

  • 查看目前在哪個 registry
$ npm get registry
$ npm set registry on http://111.11.11.111
  • 將準備好的 Library上傳到 private NPM
$ npm publish --registry http://111.11.11.111
  • 如果上傳錯誤,要移除上傳錯誤的 Library
$ npm unpublish --force myLibrary
  • 如果出現 npm ERR! request entity too large 檔案太大的訊息
    可以下
$ verdaccio

就會出現 config file — C:\Users\angel.ho.config\verdaccio\config.yaml
打開檔案加入 max_body_size 在檔案中

max_body_size: 1000mb

然後停掉 verdaccio 再重新 run 即可 publish 更大尺寸的 lib

  • 新專案準備好 React 環境之後 接著在新的空資料夾安裝此 Library
$ yarn add myLibrary
  • 此時即可完成 Components import
import React from 'react';
import { Page } from 'myLibrary/lib/components/page'
function App() {
return (
<div className="App">
<ResetStyle/>
<GlobalStyle/>
<Page/>
</div>
);
}
export default App;
  • 久未登入記得重run npm login 才能更新 Library
  • 如果 Project 被 registry 咬住,進入code ~/.npmrc 去確認目前所在 registry
Say hello! 我是 Angel,這裏的內容如果有幫到你,希望能獲得一些拍手作為鼓勵 
工作上的合作歡迎隨時透過 Mail 聯繫我 contact@aneglho.design

Thanks for watching!

--

--

Angel
Its Ok to Make Mistakes

A web / UIUX designer, in digital entertainment industry, Taipei Taiwan.