RexHung's Blog

塵世中一個前端迷途小書僮!

0%

[Tool Note] — 透過 Webpack 發布 React.js(TypeScript) 元件至 npm

Introduction & 前言

Banner

背景來至 unsplash.com 的 Chris Ried

當使用了 JavaScriptNode.js 這麼久,你肯定使用過 npm install,但這些 npm 上的套件到底是怎麼產生出來的,你可曾想過嗎?

當你開發了自己覺得超屌超猛或是別人肯定也會想試試看的程式碼之後,你知道怎麼讓大家也透過 npm install 享受到嗎?

如果你有這些問題,看這篇文章就對了,這篇文章將以 React.js 的元件當作出發點,帶你一窺怎麼透過 Webpack 把套件發布到 npm 上,讓大家也能夠安裝你的套件。

如果不會 React.js 也還是可以參考本篇文章,之後將 React.js 的部分換成你熟悉的框架或是純 JavaScript 也可以!

後續筆者也會研究其他的打包工具,例如 rollup 或者 browserify

整篇文章是筆者自己的心得,如果文內有錯誤的地方,還請盡情指出。

該篇文章參考了其他的教學,加上自己碰到的坑,整理出心得並作為筆記釋出,其中也補足了其他教學文中少的部分,而看文章的你一定會有自己的需求,請先別急,看完文章後請截取自己需要的部分,如果你覺得這篇文章對你有幫助,還請將文章分享給更多人知道,或者結合自己的心得,在發布其他更多的文章幫助更多的開發者。


Summary & 摘要

在開始前首先要非常非常感謝 稀土掘金 的 黑土豆 一篇 (建议收藏)使用React+Typescript开发组件并发布到npm仓库 文章,沒有這篇文章大概筆者還在跟 ChatGPT 有來沒回的

本篇文章預設學習前的基本條件需求:

  • 知道 React.js(不會使用也沒關係,可以換成自己熟悉的框架或者純 JavaScript 都行)
  • 電腦已經有 Node.js 環境且有 npm
  • 知道終端機及 npm 的基本操作(含安裝、初始化 package.json)
  • 大概知道 Webpack(如果不懂可以參考筆者之前的文章 [Tool Note] — 關於Webpack #1 - 第一次就上手 或是線上其他文章)
  • 一顆熱忱的心 -> 超級重要,碰到坑千萬不要氣餒

本篇文章將有以下幾個步驟:

  1. 前前前言
  2. 專案初始化
  3. 安裝相關依賴
  4. 撰寫元件
  5. Babel及Webpack設定
  6. 完善package_json
  7. TypeScript編譯
  8. 建立Demo
  9. 編譯以及Link的使用
  10. 發布前的測試
  11. 編寫README.md
  12. 發布套件
  13. 額外問題

本篇設定將以產出 ESM 為例,如果你想產出 CJS 可以參考筆者開頭提及的文章 (建议收藏)使用React+Typescript开发组件并发布到npm仓库,本篇文章也是因為參考的文章沒有 TypeScript 開啟紀錄一路


前前前言

在開始之前必須先說一下為什麼會開始做這項研究,一切開始於筆者在公司的專案,因為專案業務需求,在某一次的活動中需要刮刮樂的功能

對於前端工程師來說,如果時程不是太趕的情況下,透過需求有機會可以實作到一些平常碰不到的技術,但因為當時時間緊迫的情況下,筆者找了兩款刮刮樂的套件來使用,對我來說有輪子了,時程又趕的情況下,自行造輪子不太合理

因為筆者的專案框架為 React.js ,故找到的兩款套件為 react-scratchcard-v2scratch-card ,在剛開始安裝時,發現前者無法在手機上使用,後者則是沒有一鍵刮開所有蒙層的功能,在幾經思索之下自行使用 CanvasReact.js 製作了這個刮刮卡套件

而筆者完成功能後,開始想,會不會有其他人也剛好有這個需求,並且找不到合適的套件,所以筆者就決定將自己製作的 React.js 元件發布到 npm

或許有剛好符合功能的套件,但筆者在幾度搜尋之下只找到這兩個比較符合專案需求的套件,如果你也有找到不錯且符合筆者需求的套件歡迎下面留言分享一下

Demo

在接下來的教學之前,筆者先將專案的 Github 附上,源碼的部分其實不多,也一併分享了,想要取用也歡迎


專案初始化

在進行發布 NPM 套件之前,我們需要先初始化一個專案,該專案最終只會輸出一個 index 給安裝的人引入使用,所以請從新的資料夾開啟你的專案設定

這邊接下來都會用筆者建立的 react-scratch-ticket 刮刮樂套件作為舉例

1
2
3
mkdir react-scratch-ticket
cd react-scratch-ticket
npm init -y

如果你知道 package.json 的詳細設定,也可以使用 npm init,不要輸入 -y,關於設定可以參考 史上最強套件管理 - NPM , npm init 與 npm install (Day11)

npm init

不使用 -y 會有上圖這種詢問回答的步驟

之後會產生一個 package.json 的檔案,接下來有安裝套件就會再產生 package-lock.json ,這邊就不再多做介紹。


安裝相關依賴

安裝 React.js 依賴

因為我們專案是要發布 React.js 的套件,這邊需要安裝關於 React.js 需要的資源,當然如果你使用其他框架例如 Vue.js 或者純 JavaScript 也行

1
npm i react react-dom -D

接下來一切安裝的東西基本上都會加上 -D,因為我們的套件沒有需要在執行時會依賴到的資源,如果你有的話記得要拿掉

安裝 Webpack 依賴

由於我們會用到 Webpack 進行打包,這邊也請一起安裝,如果你使用其他打包工具的話請自行安裝

1
npm i webpack webpack-cli webpack-dev-server webpack-merge -D

安裝 TypeScript 依賴

因為筆者開發使用 TypeScript(以下開始簡稱 TS) ,這邊也需要安裝要打包 TS 的相關依賴

1
npm i typescript

安裝 CSS 依賴

也許你開發會使用到 CSS 的部分,這邊也可以一併安裝上,不安裝的話後續再 Webpack 設定請記得拿掉 CSS 編譯相關的設定

1
npm i postcss postcss-loader postcss-preset-env style-loader css-loader sass-loader node-sass mini-css-extract-plugin -D

安裝 babel 依賴

關於 babel 的介紹可以參考 Day 18 - 為什麼要用 Babel

1
npm i @babel/cli @babel/core @babel/preset-env @babel/preset-react -D

安裝 React.js 的 TypeScript依賴

因為需要用到 TS 所以也要安裝關於 React.jsTS 依賴

1
npm i @types/react @types/react-dom ts-loader @babel/preset-typescript -D

一切安裝完畢後現在 package.json 內的 devDependencies 應該長得像下面一樣,如果有少什麼再自行評斷要新增或刪除什麼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"devDependencies": {
"@babel/cli": "^7.26.4",
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"babel-loader": "^10.0.0",
"css-loader": "^7.1.2",
"mini-css-extract-plugin": "^2.9.2",
"node-sass": "^9.0.0",
"postcss": "^8.5.3",
"postcss-loader": "^8.1.1",
"postcss-preset-env": "^10.1.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.2",
"typescript": "^5.8.2",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0",
"webpack-merge": "^6.0.1"
}

撰寫元件

一切就緒之後,我們先開始撰寫元件,一些設定檔我們可以等等再開始加入,這邊先以刮刮樂為例子

關於刮刮樂的源碼,可以參考 react-scratch-ticket 內的 src

首先我們需要在專案內建立 src 資料夾,並且在專案內創建 index.tsx ,目前的架構大概會如下

1
2
3
4
5
6
├── node_modules # 安裝套件後生成
├── src # 你要發布的套件代碼精華請放這
└── index.tsx # 確保至少有一個套件進入點
├── README.md
├── package-lock.json
└── package.json

你的專案如果不是 React.js 也可以創建 index.ts 或者沒有用 TS 也可以創建 index.js ,最終我們要打包套件會需要一個入口點的

src

因為筆者預期之後會擴增刮刮樂可以引入的組件類型,例如可以一張刮刮樂多個可刮區域,所以筆者在 index.tsx 內的做法是透過他輸出 TS 以及不同元件,方便後續引用

1
2
3
4
5
6
7
// index.tsx

// Component
export { default as ReactScratchTicket } from './component/ReactScratchTicket';

// Type
export type { ScratchTicketImperative } from './component/ReactScratchTicket';

元件的部分這邊後續筆者會再補充自己的寫作想法,目前以介紹發布流程為主


Babel及Webpack設定

在放入我們要發布的元件後,我們先把一些設定檔建立好

建置 Babel 設定

先從最簡單的 babel 開始吧,這項東西是 JavaScript 的轉譯器,簡單解釋是他可以將 ECMAScript2015(ES6) 及以上的程式碼轉為向下相容的版本,讓較舊的瀏覽器也能解讀

先在根目錄創建 .babelrc ,並在裡面寫上下面內容

1
2
3
4
5
6
7
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
]
}

建置 Webpack 設定

比較重頭戲的部分都在 Webpack ,後續如果編譯失敗,或者實際安裝套件並且使用有問題,八九成都是這邊出問題

這部分筆者參考 (建议收藏)使用React+Typescript开发组件并发布到npm仓库 的文章,先建立 config 的資料夾,然後分別建立幾個檔案

建立 webpack.base.js 並寫入以下內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /(\.js(x?))|(\.ts(x?))$/,
use: [
{
loader: 'babel-loader'
}
],
exclude: /node_modules/,
}
]
},
};

建立 webpack.dev.config.js 並寫入以下內容,對於 demo 的部分等等會建立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import path from 'path';
import { fileURLToPath } from 'url';
import { merge } from 'webpack-merge';
import baseConfig from './webpack.base.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const devConfig = {
mode: 'development',
entry: path.join(__dirname, "../demo/src/index.tsx"),
output: {
path: path.join(__dirname, "../demo/src/"),
filename: "dev.js",
},
module: {
rules: [
{
test: /.s[ac]ss$/,
exclude: /.min.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: {
mode: "global"
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
],
],
},
},
},
{ loader: 'sass-loader' }
]
},
{
test: /.min.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
},
devServer: {
static: path.join(__dirname, '../demo/src/'),
compress: true,
host: '127.0.0.1',
port: 8001, // 啟動本地服務時候的端口,可以任意修改
open: true // 打開瀏覽器
},
};

const exportConfig = merge(devConfig, baseConfig);
export default exportConfig;

建立 webpack.prod.config.js 並寫入以下內容,這份才是輸入後用的,有可能你在本地使用自己寫的套件內容沒問題,但是編譯後放到其他專案去使用就出問題,大部分都需要在這邊解決

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import path from 'path';
import { fileURLToPath } from 'url';
import { merge } from 'webpack-merge';
import baseConfig from './webpack.base.js';
import MiniCssExtractPlugin from "mini-css-extract-plugin";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const esmConfig = {
mode: 'production',
entry: path.join(__dirname, "../src/index.tsx"),
output: {
path: path.join(__dirname, "../dist/"),
filename: "index.esm.js",
libraryTarget: 'module',
module: true,
},
experiments: {
outputModule: true,
},
module: {
rules: [
{
test: /.s[ac]ss$/,
exclude: /.min.css$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: {
mode: "global"
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
],
],
},
},
},
{ loader: 'sass-loader' }
]
},
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "index.min.css"
})
],
externals: {
react: 'react',
'react-dom': 'react-dom'
},
externalsType: 'module',
};

export default merge(esmConfig, baseConfig);

再次提醒,如果你沒有用到 scss 或者相關的套件,請記得要移除,反之如果有用到的部分,請自行寫入設定


完善package_json

現在我們必須完整我們的 package.json ,否則發布會失敗

基於範例套件的 package.json 如下,請補上你缺少的部分,這邊如果你不是使用 ESM 輸出,請再次參考 (建议收藏)使用React+Typescript开发组件并发布到npm仓库 的文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
"name": "react-scratch-ticket",
"version": "1.0.0",
"main": "./dist/index.esm.js",
"types": "./dist/types/index.d.ts",
"module": "./dist/index.esm.js",
"type": "module", // 不是 ESM 請不要加上這行
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --config config/webpack.dev.config.js",
"build": "webpack --config config/webpack.prod.config.js",
"build:clean": "rimraf dist && webpack --config config/webpack.prod.config.js && npx tsc", // 如果你的輸出資料夾不是 dist 請記得換名稱
"publish": "npm publish --registry https://registry.npmjs.org" // 發布的部分建議加上 --registry https://registry.npmjs.org,不然容易發生找不到問題
},
"files": [
"/dist",
"README*.md",
"LICENSE"
],
"keywords": [
"react",
"scratch",
"ticket",
"scratch-ticket",
"react-scratch-ticket",
"card",
"scratch-card",
"react-scratch-card",
"canvas",
"scratch-canvas",
"react-scratch-canvas"
],
"author": "RexHung0302",
"license": "MIT",
"description": "This is a scratch ticket component, basic on React",
"homepage": "https://github.com/RexHung0302/react-scratch-ticket#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/RexHung0302/react-scratch-ticket.git"
},
"bugs": {
"url": "https://github.com/RexHung0302/react-scratch-ticket/issues"
},
"engines": {
"npm": ">=10.0.0",
"node": ">=20.0.0"
},
"dependencies": {},
"devDependencies": {
//... 略過
}
}
  • version: 每次發布新的內容到 npm 去,在 npm run build 之後,請記得改版
  • main: 作為你發布套件後,使用者引入你套件的入口是哪個檔案,這個檔案在這個筆記中是透過 webpack 產出的
  • types: 因為筆者輸出的套件是使用 ESM,這邊需要加上這個設定,告知專案這個套件的 TS 定義去哪裡找
  • files: 發布套件後只有這邊設定到的檔案會發布上去,其餘的檔案都不會被發布到 npm 倉庫中,可參考下圖一
  • keywords: 套件的關鍵字,會出現在 npm 搜尋列表小註解,以及使用者搜尋時會 mapping 的關鍵字,可參考下圖二
  • description: 套件的說明,會出現在 npm 搜尋列表小註解,可參考下圖二

發布後的資料夾結構

發布後的資料夾結構


TypeScript編譯

開始這步驟前,筆者參考此篇文章解決許多 TS 問題 - 產生TypeScript的declare檔案

為什麼我們需要 TS 編譯呢?如果你安裝套件的環境是使用 TS ,就一定要做這件事情,確保我們要發布的套件內輸出後的資料夾含有 index.d.ts

TS not found

如果沒有就會發生像上圖的事情,找不到套件的定義

請先輸入以下指令,我們需要建立 TS 的設定檔案

1
tsc --init

如果發生 tsc 找不到的問題,可以將 TS 安裝到全域環境

1
2
npm i typescript -g

如果有產生出來或者沒有產生出來,都請在根目錄確保有 tsconfig.json 這個檔案,內容請換上下面的部分,或者可以依照個人喜好修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"compilerOptions": {
"sourceMap": true,
"target": "ESNext", // 如果你不是要產出 ESM 而是 CJS 請修改
"module": "ESNext", // 如果你不是要產出 ESM 而是 CJS 請修改
"moduleResolution": "node",
"noEmitOnError": true,
"lib": ["es2017", "DOM"],
"strict": true,
"esModuleInterop": false,
"outDir": "dist",
"rootDir": "./src",
"allowJs": true,
"noImplicitAny": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "./dist/types",
"emitDeclarationOnly": false,
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

其中尤其是 declaration、declarationMap 及 declarationDir 這三項極其重要,如果沒有這三個,你不會在執行編譯後產出 index.d.ts File

這邊的 declarationDir 輸出路徑,請記得修改的話,也要一起修改 package.jsontype 設定,兩邊一定要對得上

都設定好之後目前專案大概長這樣

1
2
3
4
5
6
7
8
9
10
11
12
├── config  # Webpack 的配置
├── webpack.base.js
├── webpack.dev.config.js
└── webpack.prod.config.js
├── node_modules # 安裝套件後生成
├── src # 你要發布的套件代碼精華請放這
└── index.tsx # 確保至少有一個套件進入點
├── README.md
├── .babelrc # babel 的配置
├── tsconfig.json # TS 的配置
├── package-lock.json
└── package.json

這邊先不要急著執行編譯指令,我們接下來將會建置 Demo 的部分,透過本地展示你這個專案的內容


建立Demo

在專案根目錄請創建 demo 資料夾,以及 src ,裡面請放上 index.tsx , 裡面可以引入你要發布的套件代碼精華 src/index.tsx

以筆者的 demo 為例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// demo/src/index.tsx
import React from "react";
import { ReactScratchTicket } from "../../src/index";
import ReactDOM from "react-dom/client";
import useIndexController from "./hook/useIndexController";
import './style.scss';

const App = () => {
const { prizeInfo, scratchTicketRef, completeHandler, initDoneHandler, resetDoneHandler, clickResetBtnHandler, clickClearCardBtnHandler } = useIndexController();

return (
<div className="container">
<h2 className="font-bold">React Scratch Ticket Demo</h2>
<p className="container__title">100X</p>
<div className="content">
<ReactScratchTicket
ref={scratchTicketRef}
containerClassName="scratch-ticket-container"
brushSize={10}
width={309}
height={52}
childrenCenter
maskingLayerImg='https://picsum.photos/309/52'
maskingLayerColor="yellow"
finishPercent={70}
onComplete={completeHandler}
onInitDone={initDoneHandler}
onResetDone={resetDoneHandler}
>
{prizeInfo.name}
</ReactScratchTicket>
</div>
<div className="buttons">
<button onClick={clickResetBtnHandler} className="button">Reset</button>
<button onClick={clickClearCardBtnHandler} className="button">Clear Card</button>
</div>
</div>
);
};

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

另外還需要創建 index.html 來預覽畫面,內容為下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- 名稱可以隨意修改 -->
<title>react-scratch-ticket-demo</title>
<style>
html, body, p {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="dev.js"></script>
</body>
</html>

根據 (建议收藏)使用React+Typescript开发组件并发布到npm仓库 的文章解釋, dev.js 不會實際在 demo/src 底下產生 dev.js ,打包好的文件是在內容存,所以實際推上去 Github 並不能直接透過 index.html 預覽,這點筆者還在研究當中

都設定好之後目前專案大概長這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├── config  # Webpack 的配置
├── webpack.base.js
├── webpack.dev.config.js
└── webpack.prod.config.js
├── demo # 本地開發預覽
└── src
├── index.tsx
├── index.html // 一定要有這個
└── index.scss
├── node_modules # 安裝套件後生成
├── src # 你要發布的套件代碼精華請放這
└── index.tsx # 確保至少有一個套件進入點
├── README.md
├── .babelrc # babel 的配置
├── tsconfig.json # TS 的配置
├── package-lock.json
└── package.json

如果有修改 demo 的相關配置,請記得一定要一併修改 /config/webpack.dev.config.js

Run Dev

這時候可以輸入 npm run dev 來查看你的成果了


編譯以及Link的使用

編譯

Run Build

當一切就緒,我們可以執行 npm run build:clean,這行指令寫在 package.json 當中,主要是要刪除舊的編譯過的檔案,然後編譯新的 JS 還有 TS 宣告內容

Dist folder

執行後就能看到資料夾多出了 /dist 資料夾,而我們 package.json 也把 main、types 以及 module 都指向 /dist

這時候可以使用 npm link 指令,這個指令是把打包後的組件引入到本機全局的 node_modules 資料夾之中

輸入之後我們就可以到 demo/src 資料夾中,輸入以下指令

1
2
cd demo/src
npm link react-scratch-ticket

請記得上面的 react-scratch-ticket 要換成你 package.jsonname

接著修改 demo/src/index.tsx 的內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// demo/src/index.tsx
import React from "react";
// import { ReactScratchTicket } from "../../src/index";
import { ReactScratchTicket } from "react-scratch-ticket"; // 引入剛剛的 link
import ReactDOM from "react-dom/client";
import useIndexController from "./hook/useIndexController";
import './style.scss';

const App = () => {
const { prizeInfo, scratchTicketRef, completeHandler, initDoneHandler, resetDoneHandler, clickResetBtnHandler, clickClearCardBtnHandler } = useIndexController();

return (
<div className="container">
<h2 className="font-bold">React Scratch Ticket Demo</h2>
<p className="container__title">100X</p>
<div className="content">
<ReactScratchTicket
ref={scratchTicketRef}
containerClassName="scratch-ticket-container"
brushSize={10}
width={309}
height={52}
childrenCenter
maskingLayerImg='https://picsum.photos/309/52'
maskingLayerColor="yellow"
finishPercent={70}
onComplete={completeHandler}
onInitDone={initDoneHandler}
onResetDone={resetDoneHandler}
>
{prizeInfo.name}
</ReactScratchTicket>
</div>
<div className="buttons">
<button onClick={clickResetBtnHandler} className="button">Reset</button>
<button onClick={clickClearCardBtnHandler} className="button">Clear Card</button>
</div>
</div>
);
};

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

這部分可以選擇性操作,不做的話也可以

npm link

如果有出現錯誤或是找不到,可以使用 npm unlink react-scratch-ticket 再重新 link 一次就行

而依據查詢到的資料,只要後續再跑過 npm run build 之後,請重新執行 npm link,而 link 的時候請務必注意你的目錄位置

關於 npm link 的相關文章可參考 如何使用 npm link 進行 node module 測試

npm link issue

後續如果你有需要在本地安裝你推的套件,根據上述文章的內容,會有需要注意的部分


發布前的測試

在發布前我們會想要測試一下編譯出來的檔案,這時候我們可以透過使用 npm pack 的方式打包出你套件

npm pack

你會發現根目錄出現一個帶有跟你 package.json 定義同樣 version 的檔案出現

這時候你可以把你打包出來的 {套件名稱}-{版本號}.tgz 複製起來,然後貼到你要使用的專案去

到你要使用的專案輸入 npm i ./{套件名稱}-{版本號}.tgz,之後就可以跟一般使用套件一樣 import { ReactScratchTicket } from 'react-scratch-ticket';

如果至此沒問題,那就可以準備發布了!


編寫README.md

這部分可以參考 (建议收藏)使用React+Typescript开发组件并发布到npm仓库 一文的 四、编写Readme文档 ,透過 readme-md-generator 可以更快的依據你 package.json 設定去產生出 README.md

在使用之前,可以先建立 LICENSE 檔案,內容可以參考其他的文章,或是筆者的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MIT License

Copyright (c) {更換成你的名稱}

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

初始化git

請初始化你的 git ,後續這個套件會推到你的倉庫去,而 npm 的介紹網頁上也會透過你 package.json 的設定連結到你的 github ,如果不知道怎麼初始化,可以參考 [Git] 初始設定

然後請建立 .gitignore 檔案,內容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/node_modules
.vscode
/dist

.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

*.tgz

.idea

都設定好之後目前專案大概長這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── config  # Webpack 的配置
├── webpack.base.js
├── webpack.dev.config.js
└── webpack.prod.config.js
├── demo # 本地開發預覽
└── src
├── index.tsx
├── index.html // 一定要有這個
└── index.scss
├── node_modules # 安裝套件後生成
├── src # 你要發布的套件代碼精華請放這
└── index.tsx # 確保至少有一個套件進入點
├── README.md
├── .gitignore
├── LICENSE // 如果沒有設定,請把 package 的 license 設定改為空字串
├── react-scratch-ticket-1.1.4.tgz // 如果有執行 npm pack 才會有該檔案
├── README.md
├── .babelrc # babel 的配置
├── tsconfig.json # TS 的配置
├── package-lock.json
└── package.json

發布套件

終於到最後我們要發布套件了,如果你還沒有 npmjs.com 的帳號,輸入以下指令

1
npm adduser

但如果你已經有帳號密碼了,可以使用

1
npm login

(/images/20250309/npm-adduser.png)

不管創建還是登入,只要你還沒有透過從專案終端機輸入指令後,通過 npmjs.com 網頁登入過,接著都會打開官網的畫面

npm login

這邊預設使用者還沒註冊過,點擊圖上的 Create Account

npm signup

輸入完資料點擊 Create Account

密碼外洩

這邊的密碼會去檢查你輸入的是不是有在網路上洩漏了,如果是請換一組密碼

信箱收驗證碼

接著這邊需要再去信箱收驗證碼,往後的每次登入也都需要收驗證碼,以此保證你的帳號安全

創建成功

創建成功後就可以關掉網頁回到你的專案了

按下 Enter

最後在專案終端機應該會出現這些文字

接著我們就可以輸入 npm run publish

發布專案

上面的內容跑完就會把專案推上去了,之後請到 npmjs.com 輸入你的專案名稱進行搜尋

搜尋專案

這邊如果有 match 到關鍵字,基本上就會出現了,但如果沒有可以把 filter 過濾改為 Recently published ,可能會在第一順位看到

如果都沒有搜尋到,之後可以考慮更改 package.json 的名稱,也可以重新再推送一次看看

刪除套件

這邊要特別注意,如果後續你不管從 npmjs.com 刪除套件了,還是透過指令 npm unpublish 刪除,都需要再等上 24 小時,才能再推送一次,所以名稱跟配置一定要弄好,一切沒問題再推送


額外問題

  • 目前無法一次編譯 ESM 以及 CSJ ,這部分後續會再找方法解決

  • 目前無法透過 prod 編譯時,建立一份 js 提供給 /demo/src 使用,導致 github 沒辦法放上 demo 連結

  • 目前無法在套件內安裝 TailwindCSS ,這會使編譯後使用的專案出現錯誤,無法辨別 @import "tailwindcss";@use "tailwindcss"; 是什麼

  • 更多額外問題會之後再補上


Conclusion & 結論

這次終於研究了想了好久的 npm 套件發布,即使知道中間可能會碰到很多坑,但真的碰到 Webpack 的時候,又想起之前被支配的恐懼…

透過朋友及同事知道除了 Webpack 還有 rollupbrowserify ,後續會再另外研究一下,希望能找出更簡易打包的方式,Webpack 真的太多繁瑣的設定,如果有想用的套件就必須要另外設定 config 檔,一個沒弄好,就會使引用的專案出現錯誤。

最後希望這篇筆記能夠幫助到你,如果你有任何問題,非常歡迎在留言討論,但請理性溝通,友善交流。


參考網站