本文首發在 掘金
背景
前兩天幫前同事寫一個兼容公安內網,外網,專網的多地圖合一的地圖類庫,但是越寫越煩躁,整理一下有以下幾個痛點:
- 使用es5語法編寫javascript,語法囉嗦冗長。
- js代碼全部寫到一個文件中,沒有模塊化,項目難以維護。
- 需要手動使用壓縮工具壓縮代碼。
所以打算使用webpack作爲新輪子的打包工具。
預期目標
- 期望使用es6語法編寫插件代碼,代碼整潔易讀。
- 支持模塊化編程,項目代碼劃分清晰。
- 代碼合併打包和壓縮自動化。
- 支持啓動開發環境調試插件代碼。
- 插件支持
window
全局引用和支持commonjs
模塊導入
初始化項目文件
npm初始化
首先第一步是初始化項目。
輸入npm init
命令生成package.json
文件。
創建項目文件夾
接下來是將各個模塊的文件架創建完成。
├─ build # 存放webpack配置代碼
├─ config # 存放關鍵參數配置代碼
├─ dist # 打包後生產文件夾
├─ example # 開發環境demo代碼
├─ src # 項目源文件
├─ .npmignore #發佈npm包時忽略文件(一般用以排除node_modules文件夾)
└─ package.json # 項目信息配置文件
定義關鍵參數
創建js文件
在config
目錄下新建腳本文件
├─ config # 存放關鍵參數配置代碼
| ├─ dev.env.js # 定義開發環境的環境變量
| ├─ index.js # 開發環境和生產環境的配置webpack關鍵參數
| ├─ prod.env.js # 定義生產環境的環境變量
編寫代碼
prod.env.js
文件的代碼。
const pkg = require('../package.json') // 引入package.json文件
// 定義環境變量和版本號
// 1. 可以使用process.env.NODE_ENV語句區分是開發環境還是生產環境
// 2. 可以使用process.env.VERSION獲取當前插件版本號
module.exports = {
VERSION: JSON.stringify(pkg.version),
NODE_ENV: JSON.stringify('production')
}
dev.env.js
文件的代碼。
const merge = require('webpack-merge') // 引入webpack配置合併工具
const prodEnv = require('./prod.env') // 引入生產環境的環境變量配置
// 合併prod.env和dev.env的配置
module.exports = merge(prodEnv,{
NODE_ENV: JSON.stringify('development')
})
index.js
文件的代碼。
const path = require('path')
const { getIp } = require('../build/util') // 引入獲取本機局域網內ip地址方法
// dist文件夾地址
let distPath = path.resolve(__dirname, '../dist')
let config = {
build:{
main: './src/index.js', // 源碼入口
assetsRoot: distPath,//生產包將會被打包到/dist目錄中
devtool: 'source-map' // 生成source-map文件,它爲 bundle 添加了一個引用註釋,以便開發工具知道在哪裏可以找到它。
},
dev:{
main: './example/src/index.js', // 調試demo代碼入口
assetsRoot: distPath, //開發包將會被打包到/dist目錄中
assetsSubDirectory:'',//靜態資源存放目錄
assetsPublicPath:'/', // 公用基礎路徑,類似於html的base標籤
devtool:'eval-source-map',
host: getIp(), // WebpackDevServer 啓動的IP地址
port: 8092 // WebpackDevServer 啓動的端口號
}
}
module.exports = config
編寫開發環境webpack配置代碼
創建js文件
在build
目錄下新建webpack公用配置文件和開發環境配置文件。
├─ build # 存放webpack配置代碼
| ├─ webpack.common.js # webpack 公用基礎配置
| ├─ webpack.dev.conf.js # webpack 啓動開發環境入口
編寫代碼
webpack.common.js
文件的代碼。
將構建開發環境和構建生產代碼的公用配置抽離出來。
const path = require('path')
let srcPath = path.resolve(__dirname, '../src') // 源碼文件夾路徑
module.exports = {
context: path.resolve(__dirname, '../'), // 基礎目錄,用以解析entry入口路徑
resolve: {
extensions: ['.js'], // 自動添加拓展名,如:import './a',會自動解析爲import './a.js'
alias: {
'@': srcPath // 定義源碼文件夾路徑的別名,如:import '@',會解析爲 import 'X:xx/xx/src'
}
},
module: {
rules: [ // babel-loader,將es6轉換成es5
{
test: /\.js$/,
include: [srcPath, path.resolve(__dirname, '../example')],
loader: 'babel-loader'
}
]
}
}
webpack.dev.conf.js
文件的代碼。
定義了在調試demo中啓動開發環境的配置,使用loader
對樣式文件進行了處理,並做了一些調試信息的優化。
const merge = require('webpack-merge')
const webpack = require('webpack')
const path = require('path')
const WebpackDevServer = require("webpack-dev-server")
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
//設置全局環境變量
const env = require('../config/dev.env')
process.env.NODE_ENV = env.NODE_ENV
//引入公用配置文件
const webpackCommon = require('./webpack.common')
// 引入開發環境配置參數
const config = require('../config').dev
// 合併開發環境webpack配置和公用配置
let webpackDev = merge(webpackCommon, {
entry: {
main: config.main // 定義調試demo代碼入口
},
output: {
path: config.assetsRoot, // 內存中映射地址
filename: path.join(config.assetsSubDirectory, 'js/[name].js'), // 入口文件的文件名稱
chunkFilename: path.join(config.assetsSubDirectory, 'js/[name].js'),// 分包加載腳本的文件名稱
publicPath: config.assetsPublicPath
},
devtool: config.devtool, // 生成source-map
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.(scss|sass)$/,
exclude: /node_modules/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1024 * 10,
name: path.join(config.assetsSubDirectory, 'img/[name].[ext]')
}
}
]
},
plugins: [
// 定義環境變量,在自定義的插件腳本中可以獲取到
new webpack.DefinePlugin({
'process.env': env
}),
// 啓動開發環境時,提示更友好
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${config.host}:${config.port}`],
}
}),
// 定義入口html文件
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, `../example/index.html`),
inject: true
})
]
})
let compiler = webpack(webpackDev)
let server = new WebpackDevServer(compiler, {
quiet: true, // 除了初始啓動信息之外的任何內容都不會被打印到控制檯
host: config.host, // server的ip地址
port: config.port// server的端口號
})
server.listen(config.port, config.host, function () {
// 啓動中的提示
console.log('> Starting dev server...')
})
開啓開發環境
在example
目錄下新建js腳本,html文件和樣式文件,在這裏引入插件源碼,進行調試。
├─ example # 開發環境demo目錄
| ├─ src # 定義開發環境的環境變量
| | ├─ index.js # demo腳本可以使用es6編寫(在這裏引用src的源碼文件進行調試)
| ├─ styles # 定義demo的樣式文件
| | ├─ index.sass # demo樣式可以使用sass編寫
| ├─ index.html # demo的html入口文件
這裏我們看到開發環境開啓成功了。
編寫構建生產文件的webpack配置代碼
創建js文件
在build
目錄下新建webpack構建生產文件配置文件。
├─ build # 存放webpack配置代碼
| ├─ webpack.common.js # webpack 公用基礎配置
| ├─ webpack.dev.conf.js # webpack 啓動開發環境入口
| ├─ webpack.pro.conf.js # webpack 構建生產文件配置入口 (新建)
編寫代碼
定義了生產文件中的環境變量,已經對生產包進行了壓縮優化體積。
構建生產文件成功後會啓動生產包依賴模塊視圖, 可根據此試圖進行代碼優化。
根據output.library
的配置,可以使用多種模塊化(window, amd, commonjs)引用方式引用。
const merge = require('webpack-merge')
const webpack = require('webpack')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const webpackCommon = require('./webpack.common')
const ora = require('ora')
const chalk = require('chalk')
//設置全局環境變量
const env = require('../config/prod.env')
process.env.NODE_ENV = env.NODE_ENV
//引入構建生產文件配置
const config = require('../config').build
// 合併公用配置和構建生產文件配置
const webpackConfig = merge(webpackCommon, {
entry: {
main: config.main // src目錄下的源碼入口地址
},
output: {
path: config.assetsRoot,// 生產打包後的存放的目錄
filename: '[name].min.js', // 生產打包後的文件名稱
library: {
root: "TdrMap", // 在window對象中如何調用,如:window.TdrMap
amd: "tdr-map", // 在amd規範下使用'tdr-map名稱引用', 如:require(['tdr-map'], function(){})
commonjs: "tdr-map" // 在commonjs規範下使用'tdr-map名稱引用',如 var TdrMap = require('tdr-map')
},
libraryTarget: 'umd', // 將你的 library 暴露爲所有的模塊定義下都可運行的方式
libraryExport: "default" // 如果使用export default導出模塊的話,配置爲'default'
},
devtool: config.devtool,
plugins: [
// 定義環境變量,在自定義的插件腳本中可以獲取到
new webpack.DefinePlugin({
'process.env': env
}),
//如果你引入一個新的模塊,會導致 module id 整體發生改變,可能會導致所有文件的chunkhash發生變化
//HashedModuleIdsPlugin根據模塊的相對路徑生成一個四位數的hash作爲模塊id,這樣就算引入了新的模塊,也不會影響 module id 的值
new webpack.HashedModuleIdsPlugin(),
new ParallelUglifyPlugin({
// 緩存壓縮後的結果,下次遇到一樣的輸入時直接從緩存中獲取壓縮後的結果返回
// cacheDir 用於配置緩存存放的目錄路徑
cacheDir: 'node_modules/.uglify-cache',
sourceMap: true,
output: {
// 最緊湊的輸出
beautify: false,
// 刪除所有的註釋
comments: false
},
compress: {
// 在UglifyJs刪除沒有用到的代碼時不輸出警告
warnings: false,
// 刪除所有的 `console` 語句,可以兼容ie瀏覽器
drop_console: false,
// 內嵌定義了但是只用到一次的變量
collapse_vars: true,
// 提取出出現多次但是沒有定義成變量去引用的靜態值
reduce_vars: true
}
}),
new webpack.optimize.ModuleConcatenationPlugin(),//作用域提升 (scope hoisting)
// 查看 webpack 打包後所有組件與組件間的依賴關係,可以針對性的對過大的包進行優化
new BundleAnalyzerPlugin({
analyzerHost: '127.0.0.1', // 分析界面的啓動url地址
analyzerPort: 8888,
openAnalyzer: false
})
]
})
// 構建中的提示信息
const spinner = ora('生產文件構建中...').start()
spinner.color = 'green'
// 開始打包構建生產文件並對打包完成對最終信息進行顯示
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' 構建失敗,出現錯誤.\n'))
process.exit(1)
}
console.log(chalk.cyan(' 構建完成.\n'))
console.log(chalk.yellow(
' Tip: 生產文件存放在dist目錄下.\n'
))
})
編寫插件代碼
下面我們在src
目錄下寫幾句僞代碼,首先先創建js文件:
├─ src # 插件源碼目錄
| ├─ index.js # 源碼入口文件
| ├─ Map.js # 地圖類文件
| ├─ decorator.js # 裝飾器文件
index.js
的文件代碼
在入口文件導入地圖類,並導出,使之可被外部調用。
import MapConstructor from './Map' // 導入地圖類
export default MapConstructor
Map.js
的文件代碼
import MarkerConstructor from './Marker'
import { addVersion } from './decorator'
// 私有方法名稱
const _init = Symbol('_init')
/**
* @class 兼容三網地圖類
* @param { DOM } ele - 傳入DOM對象
* @returns { Map } 返回地圖的實例化對象
*/
@addVersion() // 爲地圖類添加版本號的裝飾器
export default class Map {
ele
map = null
constructor(ele) {
this.ele = ele
this[_init]()
}
/**
* 初始化地圖對象的方法
* @private
*/
[_init]() {
// 創建Map實例
this.map = new BMap.Map(this.ele)
console.log('初始化map對象')
}
/**
* 設置地圖中心經緯度和層級
* @public
* @param {float} lon 經度
* @param {float} lat 緯度
* @param {int} zoom 地圖層級
*/
centerAndZoom(lon = 116.404, lat = 39.915, zoom = 11){
this.map.centerAndZoom(new BMap.Point(lon, lat), zoom); // 初始化地圖,設置中心點座標和地圖級別
}
}
decorator.js
的文件代碼
process.env.VERSION
的值是在webpack.pro.conf.js
中的webpack.DefinePlugin
插件導入的。
/**
* 爲class添加版本信息的裝飾器
* @returns { Function } 返回裝飾器方法
*/
export const addVersion = () => {
return function (target){
if (typeof target !== 'function') throw new Error('this is not a constructor')
// process.env.VERSION 是webpack注入的插件版本信息
target.prototype.version = process.env.VERSION
}
}
打包生產文件
使用npm run build
就可以將打包後的代碼存放到dist
目錄下。
如何引用插件
- 使用
<script>
標籤引入打包後的main.min.js
文件可以直接在window
對象下面直接實例化TdrMap
類。
<script src="./main.min.js"></script>
- 使用
commonjs
模塊規範引入的腳本。(我們必須先在package.json
文件下定義main
屬性的值爲dist/main.min.js
,表示這個插件的入口文件是main.min.js
文件),然後將你開發完成的包上傳到npm
上(如何將包上傳到npm),最後使用npm install xxx
安裝,即可在代碼中引用。
import TdrMap from 'tdr-map'
測試打包後的生產文件是否能調用成功
最後看到TdrMap類已經可以成功實例化了,插件的版本信息也成功打印出來了。
最後
本項目的使用的webpack
爲3.10.0
, 如果要照着我的配置寫的話,建議使用和我同樣的版本號,另外其他依賴模塊的版本號可以參考我package.json
文件中的devDependencies
。
項目文件將做爲種子文件:github。
謝謝支持
您的支持是我繼續寫作的動力。