最近我們小組內打算推進一個內部私有庫的專案,實現內部元件、模組、類庫的中心化管理,方便小組內成員使用。私有庫是基於第三方庫sinopia搭建的,但本文不涉及該庫的構建,主要討論內容是自定義npm模組包從打包到釋出的一些心得。
為什麼要使用webpack對元件或者模組進行打包?因為可複用庫的模組化,需要適合在任何場景中進行引用,比如AMD/CMD、CommonJs、ES6、ES5等環境。從webpack打包之後的標頭檔案來看:
(function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object')
module.exports = factory(); // node
else if (typeof define === 'function' && define.amd)
define([], factory); // AMD/CMD
else if (typeof exports === 'object')
exports["Url"] = factory();
else
root["Url"] = factory();
})(this, function () {
// somecode
}
從程式碼可以看出,webpack打包出來的檔案是支援多場景的引用方式的。
筆者使用了一個測試包來分析。先上程式碼:
這是package檔案
// package.json
{
"name": "Url",
"version": "1.0.0",
"description": "測試模組",
"main": "./build/Url.min.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {},
"keywords": [],
"author": "nardo.li",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-preset-es2015": "^6.24.1",
"webpack": "^3.1.0"
}
}
這是
webpack
配置檔案(babel
和copy
的外掛其實沒用到)
其中output的配置項裡需要寫入libraryTarget: 'umd'
const webpack = require('webpack')
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
context: path.resolve(__dirname, './lib'),
entry: {
Url: './Url.js'
},
output: {
path: path.resolve(__dirname, './build'),
filename: '[name].min.js',
libraryTarget: 'umd'
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015']
}
}]
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, './lib'),
to: path.resolve(__dirname, './build'),
force: true,
toType: 'dir',
ignore: ['.*']
}
])
]
}
這是模組檔案(隨便寫了一個)
// Url.js
function Url () {}
Url.prototype.getParams = function (URL, key) {
if (key) {
let reg = new RegExp(key + '=([^&]+)')
let val = (URL.match(reg) || ['', undefined])[1]
return val
} else {
let _options = {}, urlStr = ''
if (URL.indexOf('?') > 0) {
urlStr = /\?([\S]*)/.exec(URL)[1]
urlStr.split('&').forEach(function (item, i) {
_options[item.split('=')[0]] = item.split('=')[1]
})
}
return _options
}
}
module.exports = new Url()
webpack
內部模組的引用和輸出使用的是CommonJs規範(也就是node的模組),所以模組輸出方式遵照預設規範module.exports = new Url()
接下來,打包編譯。執行$ webpack
命令,打包成功。
為了方便測試,直接將編譯好的檔案複製一份放到筆者選擇最近手上在做的專案中,專案正好使用的是vue
框架。
啟動服務,專案中引用方式如下:
import Url from './modules/Url'
控制檯列印:
Url undefined
筆者當時的疑問是,難道打包好的檔案沒有成功輸出嗎?於是換用了node
模組引入的方式:
控制檯列印:
Url Object{Url: Url, __esModule: true}
可以看到,這種方式輸出有值,但似乎是掛載到輸出物件的Url
屬性上,這種輸出形式與es6
的export Url
有點類似。於是乎,筆者猜想,是否像es6
未指定default
物件時,需要採用{ Url }
解構賦值的方式從輸出物件的引用中獲取到需要的值。
接下來再換了兩種方式:
不出所料,控制檯列印:
Url Url{}
看到這,有點搞清楚了,再回到打包好後的檔案頭部:
新增輸出日誌,發現控制檯列印:
似乎明白了,最終輸出方式的不同,應該是webpack切割檔案時導致的。在這個專案中,打包好的Url.js檔案裡的自呼叫函式傳入的this
指向了webpack
定義的一個全域性物件,用來掛載輸出。而引入未編譯好的模組包,則不會有這種問題。
為什麼會出現這個問題?筆者決定先將引入的Url模組包換成未編譯時的檔案Url2.js
,然後將整個專案打包:
// Url2.js
function Url() { }
Url.prototype.getParams = function (URL, key) {
if (key) {
let reg = new RegExp(key + '=([^&]+)')
let val = (URL.match(reg) || ['', undefined])[1]
return val
} else {
let _options = {}, urlStr = ''
if (URL.indexOf('?') > 0) {
urlStr = /\?([\S]*)/.exec(URL)[1]
urlStr.split('&').forEach(function (item, i) {
_options[item.split('=')[0]] = item.split('=')[1]
})
}
return _options
}
}
module.exports = new Url()
然後看打包後的app.js
:
可以看到webpack切割之後的對應的Url輸出方式仍然為module.exports
,與其他模組包無異。
然後換成編譯後的Url.js
,再次打包:
是不是一目瞭然了,最終輸出該檔案時沒有傳入exports
物件,所以採用了掛載全域性的形式。
簡單來看,是因為webpack對已編譯過的模組和未編譯的模組的切割方式不同。而根本原因是,webpack在打包時能識別依賴模組是否符合UMD規範,如果不是UMD規範模組(比如註明的jQuery),或編譯過的模組,都會被判斷為非規範模組,以全域性掛載的方式輸出。
如果這個編譯後的Url.js
直接在一個靜態html
檔案中採用如下方式引用:
<script src="../src/modules/Url.js"></script>
<script>
console.log('Url', Url)
</script>
控制檯列印:
可以看到,this
指向window
,所以輸出值掛載在了window
物件。
結論
綜上所述,經過webpack打包後的模組,可以通過多種方式引用,這也是因為webpack
的編譯過程跟編譯後如何被其他檔案引用沒有關係,只是跟js執行時的環境有關。一般來說Server端node場景不需要引入編譯好的模組,而在使用vue
等框架開發業務時,通也會結合webpack,所以引入的模組其實也不需要提前編譯好。
之所以需要對這些業務元件進行打包編譯,首先是因為在小組提供的公用元件中有不少是依賴於第三方庫,其次是因為有不少專案應用場景還是沿用的以script
標籤載入js資源的方式。