組內負責的幾個專案都有一些一樣的公共元件,所以就著手搭建了個公共元件開發腳手架,第一次開發 library,所以是參考著 iview 的配置來搭建的。記錄如何使用webpack4
搭建一個library
的腳手架
前言
使用 webpack4,需要安裝 webpack 和 webpack-cli
yarn add webpack webpack-cli -D
複製程式碼
然後就是書寫配置檔案。
專案腳手架結構
我寫的 library 的目錄結構如下,僅供參考,主要是模仿 iview
的結構,其中部分配置參考了 vue-cli
的 webpack 配置檔案。
├─build
│ build.js // 用於執行構建
│ check-versions.js // vue-cli 留下的,主要就是檢查npm版本和node版本
│ webpack.base.conf.js // 通用配置
│ webpack.dev.conf.js // 開發環境
│ webpack.dist.prod.conf.js // 用於生成library的程式碼 -- hbf.min.js
│ webpack.prod.conf.js // 用於生成example檔案的打包程式碼,這個其實是沒有必要的.
│
├─dist
│ └─example // example生成的打包資料夾,可以通過githubPage來預覽,或者本地使用anywhere預覽
│ hbf.min.js // library 檔案
│
├─example // example目錄
│ App.vue
│ index.html
│ main.js
│
├─lib
│ │ index.js // 全量引入公共元件,並暴露出來,包含install方法可供vue引入使用該外掛
│ │ README.md
│ │
│ └─components // 公共元件
│
├─package.json // 專案包依賴
複製程式碼
更加具體的資訊可以到github倉庫閱覽。
經過 webpack 編譯後的程式碼
為了更好的理解,先來了解下 webpack 編譯後的程式碼。
經過webpack處理過的程式碼通常都是如下所示
// webpack編譯後的程式碼
/*
* @param {Array} modules
*/
;(function(modules) {
function __webpack_require__(moduleId) {
var module = {
i: moduleId, // 模組ID
l: false,
exports: {}, // 作為結果返回.
}
// 呼叫modules陣列的某個元素(型別為函式)
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
)
return module.exports
}
return __webpack_require__(0)
})([
/** 省略了程式碼, 該陣列的每一項代表一個模組,實際是一個函式,接受三個引數,module物件,module.exports物件,__webpack_require__函式 **/
])
複製程式碼
webpack 編譯後的程式碼的整體結構就是一個IIFE函式
,接受一個 modules: Array
引數。
對於模組處理,無論是 ES Module
的 import
還是 commonjs
的 require
都轉化為__webpack_require__
這個函式來引入模組。
__webpack_require__
函式,會從 modules
陣列的第一個元素開始(moduleId 為 0,也就是入口檔案),執行該模組(實為一個函式)的邏輯,利用傳入的module.exports
的資料型別為引用型別Object
,間接地給module.exports
新增屬性。
return __webpack_require__(0)
從入口檔案開始,逐個引入依賴模組,最後返回入口模組的 module.exports
此時這個編譯後的 js 檔案,是無法被其他模組所引用的,只在當前作用域內有效, webpack
就提供了建立 library 的方式,就是在output
裡定義library
和 libraryTarget
。使得構建完的 js 可以供其他模組引入使用。
設定library配置
對於作為一個 library 使用的專案來說,output 選項需要設定 library
// webpack.dist.pord.conf.js
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: 'hbf.min.js',
library: 'hbf',
libraryTarget: 'umd'
},
複製程式碼
library
可以是字串,也可以是物件,(物件僅限於 libraryTarget
的值為 umd
的情況下使用)
output: {
library: {
root:'Hbf', // 暴露給全域性變數,window.Hbf進行呼叫
commonjs: 'hbf-public-components'
},
libraryTarget: 'umd'
}
複製程式碼
commonjs
和 commonjs2
的區別。
commonjs
規範就是定義了一個 exports
物件,而 nodejs
在實現的時候,在 commonjs
規範的前提下做了一些擴充套件,定義了 module.exports
,從而也叫這種為 commonjs2
規範。
我們在引用別人的庫的時候,通常都是可以通過多種方法引入的,比如 <script>
標籤引入,通過 commonJS
模組 引入,通過 ES6 Module
引入。
libraryTarget
設定為umd
(通用模組規範)的話,則打包後可以通過多種模組載入的方法載入 library,具有高相容性。
關於libraryTarget
詳細要點可以參考webpack官方文件
library 的依賴問題
如果我們的 library 是基於某某庫的基礎上開發的,比如說寫一個基於vue
的 UI 元件庫,在開發的這個元件庫的時候,我們需要引入vue
,如果使用這個元件庫的使用者本身就已經引入了vue
,那麼vue
就會被引入並打包兩次,所以我們在開發一個library
的時候,對於一些所依賴的模組,可以由引入library
的使用者提供。所以我們需要將依賴的模組在 library 的打包構建中去除。
externals
的作用,防止將某些 import
的包打包到 bundle 中,而是在執行時再去外部獲取這些擴充套件依賴。
通過設定 externals
,從輸出的 bundle
中排除 vue
和 iview
。
這些外部依賴可能是以下的任何一種形式。
root
全域性變數訪問commonjs
作為一個commonjs
模組引入commonjs2
與commonjs
類似,不過匯出的是module.exports
amd
使用amd
模組規範引入
// webpack.dist.pord.conf.js
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
},
iview: {
root: 'iView',
commonjs: 'iview',
commonjs2: 'iview',
amd: 'iview'
}
},
複製程式碼
另外在 package.json 加多一個peerDependencies
欄位,作用是約定library
所依賴的庫的版本號,在使用者下載使用library
的時候,如果所依賴的 iview
和 vue
的版本號不對,就會發出警告。
// package.json
"peerDependencies": {
"iview": ">2.0.0",
"vue": ">2.0.0"
},
複製程式碼
對於這兩個依賴,寫到開發環境依賴中 ,不然安裝時,會在庫的目錄下安裝vue
和 iview
,這也不符合讓 library
的引用者提供 library
的依賴這個想法。
vue 外掛庫/ 元件庫
對於 vue 的外掛庫 / 元件庫來說,如果想要全域性引入的話,需要有一個install
方法。install 內部邏輯就是通過引數傳進來的vue
物件,註冊所有元件。然後最後將所有公共元件連同install
方法組成一個新物件暴露出去。
// 引入公共元件
import publicMenu from './components/public-menu'
import tablePage from './components/table-page'
import sliderCustom from './components/slider-custom'
const components = {
publicMenu,
tablePage,
sliderCustom,
}
const Hbf = Object.assign({}, components)
const install = function(Vue, opts) {
if (install.installed) return
Object.keys(components).forEach(component => {
Vue.component(component, component)
})
}
// 用於script標籤引入
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
// 將install方法賦給Hbf物件
Hbf.install = install
// 輸出default變數,用於全量引入,也可以在引入的時候選擇使用 * 來全量引入
export default Hbf
// 輸出各個元件,用於按需引入
export { publicMenu, tablePage, sliderCustom }
複製程式碼
在暴露含有 install 方法的物件時,一開始使用的是 module.exports
,引用 library 的時候報錯Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'
。
因為我測試用的專案關閉了 babel
對 ES Module
的編譯,通常情況下,沒有手動關閉的話,babel
會將 ES6 Module
編譯轉換成 commonjs
規範。所以在關閉了module
的轉換的情況下,由於庫的輸出使用的是 commonjs
規範的 module.exports
,而引入庫使用的是 ES6 模組規範的import
關鍵字,所以產生了報錯,我將庫的匯出寫成export
關鍵字,就沒報錯了。
瞭解到了現在大多數庫,都是用的 commonjs
規範,由於 webpack
的 tree-shaking
只對 ES Module
起作用。而webpack
的 tree-shaking
實際上是由Uglylify
來實現的。
所以庫的模組規範可以使用兩種,利用 package.json
的 main
和 module
欄位分別定義庫的兩種模組規範的入口檔案。 main
使用的是 commonjs
規範語法書寫的檔案,而 module
是使用 ES6 module
語法書寫的檔案,module
欄位目前還是一個提案。所以採用了 ES2015
模組語法的庫的,當我們只使用到 library
的部分程式碼,則可以利用webpack
進行 tree-shaking
,去除未引用的程式碼,減少打包檔案體積。
釋出 npm 包
首先就是需要註冊一個 npm 賬號。
如果之前使用的是淘寶映象的話,需要先切回 npm 官方源。不然是發不了包的。
開啟命令列
切換官方源 npm config set registry https://registry.npmjs.org/
執行npm login
,然後輸入你的賬號資訊。
可以配置.npmignore
忽略一些不需要上傳的檔案,寫法跟.gitignore
相同。
需要保證專案有正確的package.json
檔案和README.md
檔案
然後執行npm publish
進行發包。
發完包就可以切回淘寶映象源
npm config set registry https://registry.npm.taobao.org
引用元件庫
Script 標籤引入
一開始是將 JS 檔案放在本地測試,發現在HTML
檔案的第一行就報錯,Unexpected token <
,StackOverflow說的原因是引入路徑不正確,所以我就把 JS 檔案放到 CDN 上了,
<script src="http://osuuzm0m8.bkt.clouddn.com/hbf.min.js"></script>
<script>
console.log(window.Hbf) // 會看到你匯出的物件
</script>
複製程式碼
輸出
全量引用
可以像使用其他 vue 外掛庫/元件庫一樣使用。
import hbf from 'hbf-public-components'
// 使用use方法觸發hbf的intall方法,註冊全部元件
Vue.use(hbf)
複製程式碼
如果是沒有匯出default
變數,則使用另外一種方式全量引入
import * as hbf from 'hbf-public-components'
複製程式碼
按需引用
import { publicMenu } from 'hbf-public-components'
複製程式碼
按需引用,如果 library 使用的是ES2015 Module
規範,則不需要安裝任何外掛,webpack 會對其進行tree-shaking
,去除未引用的程式碼。
前面提過,webpack
的tree-shaking
是由Uglylify
外掛實現的,我在開發環境下,沒有啟用Uglylify
來壓縮程式碼,所以檢視模組打包圖,會發現整個庫都被引入了,雖然我只引入了一個元件。webpack4
在生產環境下,才會進行tree-shaking
,設定mode
的值為production
就會開啟生產環境下的優化。
如果是使用 commonjs
規範的 library 則需要一個外掛支援,babel-plugin-import。該外掛是ant
官方開發的。許多 UI 元件庫的按需引入也是依賴於這個外掛。
安裝
yarn add babel-plugin-import -D
修改.babelrc
檔案,
"plugins": [
["import", {
"libraryName": "hbf-public-components",
"libraryDirectory": "lib/components"
}]
],
複製程式碼
總結
其實如果對於一個不是很穩定,需要一直迭代更新
的公用元件庫
來說,使用 npm 包的話,會比較不方便,經常更新的公共元件程式碼可以使用Git subtree
(教程)來維護,可以等到一定地步之後,公共元件庫穩定下來之後再考慮釋出一個 npm 包。
而開發一個元件庫,也可以使用 rollup.js 來搭腳手架,rollup.js
預設使用的就是 ES2015 Module
,可以進行靜態分析,去除未引用的程式碼,tree-shaking
也是 rollup.js
先提出的。Rollup
相比較於Webpack
,更適合用於構建library
,Vue.js
就是使用 Rollup
構建的。Webpack
在程式碼分割這方面比較有優勢,所以webpack
相對來說比較適合構建應用程式,不過使用 webpack 構建 library 也是可以的。
這個專案也可以用做 webpack 構建 library 的通用腳手架。下次再嘗試Rollup
構建 library
有問題一起探討。
專案地址: github
npm 地址:npm
如果對於webpack和babel對ES Module
的處理不是很熟悉的可以閱讀一下下面這篇文章,很nice。