用Webpack開發Vue Plugin並公開發佈到npm上
開始之前
會想寫這篇
是因為大部分的經驗都是使用別人的套件
有時候自己想做一些套件出來讓自己或更多人能使用
而我在開發Vue Plugin的過程遇到滿多問題
包含發佈到npm上的時候也是
所以特別開一篇文章來記錄一下
實做Repo: https://github.com/ciao-chung/vue-plugin-example
主要流程
大致的流程如下
- 用vue-cli建立webpack專案
- 設定Github Demo Page, 讓目前dev模式直接可以變成Demo Web
- 開始開發套件
- 設定套件publish模式(注意這邊是套件的publish, 不是整個Web的production)
- 將babel-loader設定獨立抽出
- 建立套件publish的相關設定
- 發佈到npm
用vue-cli建立webpack專案
這邊就只是單純用vue-cli打出基本的webpack樣板
vue-cli的使用方式就不多做說明
vue init webpack vue-plugin-example
設定Demo Page
這邊的Demo Page除了開發的時候可以拿來測試功能
最後還能以Github Page形式變成Demo或文件網站
就看需求來決定用途
先簡單做一個Demo Page就好
<template>
<div>
<h1>Demo Page</h1>
</div>
</template>
<script>
export default {}
</script>
<style lang="sass" type="text/sass" scoped></style>
修改webpack production設定
如果沒有要用Github Page來當Demo Page或文件網站的話
可以直接略過這一步驟
這邊我是希望將production的輸出資料夾名稱改成docs(vue-cli預設打出來是dist)
而docs資料夾可以直接用來當Github Page(參考文件)
因此需要對webpack production設定做一些修改
在config/index.js中找到build property
接著修改裡面的index與assetsRoot
從dist改成docs
如果你的Github Page的domain不是自訂而是預設的
那這邊還需要修改assetsPublicPath
這步驟很重要
因為官方預設的Github Page結構大概是這樣https://username.github.io/repo-name
如果沒修改assetsPublicPath的話打包出來的index.html會找不到其他的js跟css檔
整個web會直接空白給你看
所以這一步我們需要將assetsPublicPath改成repo name
這邊結尾記得要補斜線
測試Github Page
這邊跟上一步驟一樣
如果沒有要用Github Page來當Demo Page或文件網站的話
可以直接略過這一步驟
這時候我們可以先執行打包測試輸入資料夾名稱是不是docs
npm run build
確定輸出成功後就沒問題了
這時候可以直接push到Github上
整個docs資料夾給他commit然後push上Github就對了
push完成後
接著登入Github進入該repo的設定頁
設定Github Page選項
選擇第二個把master下的docs資料夾當然Github Page的選項並儲存
另外我有發現這個選項是要在master有docs資料夾的情況下才會出現
所以要先push才有得選
儲存成功後就會看到Github Page的網址了
直接點進去看
如果是剛才設定的Demo Page那就代表ok了
之後只要想發佈新的Demo Page或文件網站
只要執行npm run build
再commit + push就可以了
開始開發套件
(這部份可參考src下的結構)
因為這邊主要是要介紹Vue Plugin的開發過程
所以plugin就用個最陽春最簡單的dialog來示範就好
從官方文件可以看到Vue Plugin可以分為四種方式被安裝
並經由Vue.use method使用該套件
這邊我選擇第四種
也就是直接擴充Vue的prototype
讓每個Vue的實體可以直接call這個method
這是最主要的Installer.js
到時候發佈package的進入點就是指向這個js
而在Demo Page中也是載入這個js來安裝套件測試
import Dialog from '@/components/Plugin/Dialog.vue'
import Vue from 'vue'
import { events } from '@/components/Plugin/Events.js'
class Installer {
constructor() {
this.isInstalled = false
}
install(Vue, options) {
if(this.isInstalled) return
this.isInstalled = true
// 載入主要的dialog元件
Vue.component('VueDialogPlugin', Dialog)
Vue.prototype.$dialog = (options) => {
events.$emit('dialog', options)
}
}
}
export default new Installer()
接著是Installer.js裡面的Events.js
這是為了使用Vue的event bus來做套件的觸發
import Vue from 'vue'
export const events = new Vue()
再來是最重要的套件本體Dialog.vue
主要是監聽Installer.js裡面定義的event
被觸發時做切換dialog動作
<template>
<div vue-dialog v-if="active">
<h2>{{title}}</h2>
<div>{{content}}</div>
<button @click="close">close</button>
</div>
</template>
<script>
import { events } from '@/components/Plugin/Events.js'
export default {
data() {
return {
title: null,
content: null,
active: false,
}
},
async created() {
events.$on('dialog', this.open)
},
methods: {
open(options) {
// 從Demo.vue經由installer傳來的options
this.active = true
this.title = options.title || 'Title'
this.content = options.content || null
},
close() {
this.active = false
},
},
}
</script>
<style lang="sass" type="text/sass" scoped>
div[vue-dialog]
position: fixed
z-index: 5000
top: 100px
left: calc(100vw/2 - 400px/2)
border: 2px gray solid
width: 400px
height: 300px
padding: 15px
</style>
最後在App.vue安裝套件
<template>
<div id="app">
<router-view/>
<!--依照Installer.js訂的元件名稱掛載元件-->
<VueDialogPlugin/>
</div>
</template>
<script>
// 安裝Plugin,
import Vue from 'vue'
import Dialog from '@/components/Plugin/Installer.js'
Vue.use(Dialog)
export default {
name: 'App'
}
</script>
<style lang="sass" type="text/sass" scoped></style>
並且在Demo.vue裡面加入測試按鈕
<template>
<div>
<h1>Demo Page</h1>
<!--測試按鈕, 點選後開啟dialog-->
<button @click="open">open</button>
</div>
</template>
<script>
export default {
methods: {
open() {
this.$dialog({
title: 'foo',
content: 'bar',
})
},
},
}
</script>
到這邊可以算是全部開發完了
再執行打包一次然後commit push
就可以在Demo Github Page上測試套件
新增套件的publish
前面的npm run build
是Demo Page的production
而這邊是要處理套件本身的publish
從vue-cli的webpack template結構中
我們可以看出有dev跟production模式(如果vue-cli裝webpack template時有安裝測試功能, 還會多一個test模式)
現在我們需要再加一個套件的publish模式
建立publish模式的話一開始可以先直接複製production設定
以下是大概要建(ㄈㄨˋ)立(ㄓˋ)的幾個設定
- 從build/webpack.prod.conf.js複製一個webpack.publish.conf.js檔案
- 從build/build.js複製一個build/publish.js檔案
- 從config/prod.env.js複製一個config/publish.env.js檔案(裡面參數記得也要改成publish)
- 最後config/index.js裡面的build property也要記得複製出一個publish property
接著我們在package.json加入發佈套件的npm script
也就是執行剛才建立的build/publish.js
{
// ...
"scripts": {
// ...
"publish-plugin": "node build/publish.js"
}
}
修改vue-loader.conf.js裡面的isProduction
這邊我們需要在production模式與publish模式中使用extract選項
所以要修改一下isProduction
const isProduction = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'publish'
設定index.js的publish property
這邊主要是設定publish模式的一些路徑
刪掉index property
首先可以直接將index property刪掉
因為我們只是要打包一個Vue Plugin(進入點是src/components/Plugin/Installer.js)
所以我們不需要打出index.html
將輸出資料夾改成publish
我希望publish模式輸出成一個publish資料夾
所以這邊要修改assetsRoot property
assetsRoot: path.resolve(__dirname, '../publish'),
將assetsPublicPath改成root
前面的assetsPublicPath設定成/vue-plugin-example/是為了Github Page而設定的
這邊要改成root
assetsPublicPath: '/',
差異
設定build/webpack.publish.conf.js
這個檔案是publish模式的webpack設定
有些設定是為了覆蓋掉webpack.base.conf.js的設定
env改成載入config/publish.env.js
const env = require('../config/publish.env')
把config的設定全部改成publish
首先先將全部的config.build設定整批換成config.publish
這個config就是require config/index.js的設定
也就是上個步驟修改的publish相關路徑
所以我們全都要改成publish的設定
打包時排除vue避免檔案過大
這邊可以使用webpack的externals選項
打包時將vue排除
externals: [/^vue$/i],
設定進入點
接著我們要設定entry propery改成由src/components/Plugin/Installer.js進入
覆蓋掉webpack.base.conf.js中的entry property
也就是publish後
使用者安裝這個套件
將會直接載入Installer.js
entry: './src/components/Plugin/Installer.js',
修改output property
把原本的chunkFilename property拿掉
並且直接將filename指定成dist/vue-plugin-example.js
到時候將會輸出在publish/dist資料夾中
接著加上libraryTarget: 'umd'
設定
這邊是使用webpack的output libraryTarget選項
主要是將這個library設定成能在CommonJS中使用
也就是打包的時候webpack會去幫我們處理ES6/ES7的code
output: {
path: config.publish.assetsRoot,
filename: 'dist/vue-plugin-example.js',
libraryTarget: 'umd',
},
修改ExtractTextPlugin設定
原本的設定太過複雜會打包成很多支css檔
我這邊希望只要打成一支css讓使用套件的人方便載入
new ExtractTextPlugin({
filename: 'dist/vue-plugin-example.css',
}),
拿掉所有CommonsChunkPlugin
直接把所有CommonsChunkPlugin都拿掉
原因跟上一點一樣
我只想打出一支js讓使用者方便載入
拿掉HtmlWebpackPlugin的設定
因為這是一個Vue Plugin
程式的進入點是一支js
所以我們要把原本的HtmlWebpackPlugin直接拿掉
不然publish的時候會跟production一樣打出index.html
設定build/publish.js
以原本build/build.js來說
它是開始執行production打包的進入點
而我們現在需要修改build/publish.js
改成使用上面修改完的幾個設定來執行打包
把config的設定全部改成publish
首先一樣先將全部的config.build設定整批換成config.publish
全都改成publish的設定
環境變數改成publish
process.env.NODE_ENV = 'publish'
webpackConfig改成載入前面設定的webpack.publish.conf.js
const webpackConfig = require('./webpack.publish.conf')
差異
直接執行publish script看看輸出結構是否正常
上面的步驟都設定完後
我們就可以直接執行前面新建立的publish script來看看打包出來的結果是否正確
npm run publish-plugin
從輸出結果來看
我們可以看到有依照我們的設定
在publish/dist打出一支js跟map檔
將babel-loader設定獨立抽出
這步驟是為了改善publish打包壓縮
如果這個步驟沒做的話套件仍然可以正常發佈使用
雖然前面的步驟已經可以成功打包出套件
不過套件的壓縮還是可以再改善
因為vue-cli webpack template中的babel-loader設定是使用.babelrc
而裡面使用的transform-runtime
會在publish打包時做加入babel polyfill而造成檔案過大
因此如果將babel-loader的設定抽出來動態載入transform-runtime
這樣打包出來的套件size將會很明顯的被壓縮
建立babel-loader的獨立設定檔
直接在build資料夾下建立一個babel-loader.conf.js
另外constructor傳入的resolve是因為不想在重複建立build/webpack.base.conf.js的那個resolve method
所以直接從外部注入使用
options就直接沿用原本.babelrc的內容
plugin先預設不使用transform-runtime
class BabelLoaderConfig {
constructor(resolve) {
const isPublish = process.env.NODE_ENV === 'publish'
this.config = {
test: /\.js$/,
loader: 'babel-loader',
include: [
resolve('src'),
resolve('test'),
resolve('node_modules/webpack-dev-server/client')
],
options: {
presets: [
['env', {
modules: false,
targets: {
browsers: ['> 1%', 'last 2 versions', 'not ie <= 8']
}
}],
'stage-2'
],
plugins: ['transform-vue-jsx']
}
}
// 只有不是在套件publish的時候才使用transform-runtime
if(!isPublish) this.config.options.plugins.push('transform-runtime')
}
exportConfig() {
return this.config
}
}
module.exports = function (resolve) {
return new BabelLoaderConfig(resolve).exportConfig()
}
修改build/webpack.base.conf.js
直接將原本的babel部份取代成新的babel設定(記得要注入resolve)
刪掉.babelrc
既然已經將babel-loader的設定抽出來了
直接刪掉.babelrc
不然babel-loader還會一直讀取原本的.babelrc設定
比較一下壓縮後的大小
圖片上方是原本vue-cli webpack template的.babelrc打出來的結果
下方則是修改完babel-loader設定之後的打包結果
可以看到vue-plugin-example.js的大小直接從29.4k變成3.09k
幾乎只有原本的十分之一
而map檔也是將近變成原來的十分之一
在dev模式載入打包好的套件測試
接著我們可以直接把App.vue的components/Plugin/Installer.js換成剛才打出來的publish/dist/vue-plugin-example.js
直接在dev測試剛才打出來的檔案是否能被正常使用
<template>
<div id="app">
<router-view/>
<VueDialogPlugin/>
</div>
</template>
<script>
import Vue from 'vue'
import Dialog from '@/../publish/dist/vue-plugin-example.js'
Vue.use(Dialog)
export default {
name: 'App'
}
</script>
<!--css也要記得載入-->
<style src="@/../publish/dist/vue-plugin-example.css"></style>
如果換完之後在dev模式可以正常使用該plugin開發的功能
那就代表publish script打出來的檔案沒問題
可以準備發佈套件了
加入套件的package設定
在將套件發佈到npm之前
我們要先設定這個package
設定package
進到將要發佈的publish資料夾中
執行npm init
或yarn init
進行該套件的初始化
這邊要注意的是entry point要記得設定為dist/vue-plugin-example.js
加入相依性
如果你開發的套件有相依其他的套件
就要在package.json加上相依性
如果是用yarn的話也要更新yarn.lock
不過目前這個範例沒有相依其他套件
所以就可以略過這個步驟
設定.npmignore
通常我們需要排除掉一些檔案或資料夾
避免跟著被發佈到npm上
這時候我們就需要從.npmignore上設定
node_modules/*
.idea/*
npm-debug.log
example/*
example/.*
.DS_Store
node_modules/
yarn-debug.log*
yarn-error.log*
/test/unit/coverage/
/test/e2e/reports/
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
設定.gitignore
通常要發佈到npm的publish資料夾中
除了package.json、yarn.lock、.npmignore這三個檔案之外
其他都不需要進git
所以我們要針對publish資料夾做.gitingore的設定
publish/*
!publish/package.json
!publish/yarn.lock
!publish/.npmignore
發佈到npm
這發佈之前要先註冊npm帳號
接著進到publish資料夾中
先執行npm login
登入npm帳號
接著執行npm publish後
只要看到下列訊息
就代表套件發佈成功
上npm搜尋套件名稱應該也可以正常搜到
之後如果要更新版本
只需要更新package.json裡的version
再次重複上面的動作
就能更新套件的版本了
其他可能遇到的問題或需求
這邊主要是一些可能會遇到的問題或需求
每次publish的時執行一些shell
假設我希望每次在publish打包之前把整個publish/dist資料夾刪除
並且在publish打包成功之後複製README.md到publish中讓npm中永遠有最新的文件
這時候可以在build/webpack.publish.conf.js中使用webpack-shell-plugin
並且加入設定
new WebpackShellPlugin({
onBuildStart: [
'rm -rf publish/dist/',
],
onBuildEnd: [
'cp README.md publish/'
],
}),
完整設定請參考build/webpack.publish.conf.js
npm publish顯示成功卻在其他專案裝不起來
大部分時候都沒這個問題
不過有一次發佈一個套件
在npm的package list中明明有看到發佈的專案成功
但是不管是用npm還是yarn都裝不起來
在https://registry.npmjs.org中找也是404(可以使用https://registry.npmjs.org/package-name來查詢package資訊)
結果過了兩三個小時候就可以裝
我猜應該是那個時候太多人同時發佈
npm可能publish後要先進佇列
人多塞車才會等久一點
所以遇到這個問題可以先等個幾小時看看
真的太久沒有的話
再寫信給npm問吧