配置webpack作為你新輪子的打包工具

林楠發表於2019-01-03

背景

前兩天幫前同事寫一個相容公安內網,外網,專網的多地圖合一的地圖類庫,但是越寫越煩躁,整理一下有以下幾個痛點:

  1. 使用es5語法編寫javascript,語法囉嗦冗長
  2. js程式碼全部寫到一個檔案中,沒有模組化,專案難以維護
  3. 需要手動使用壓縮工具壓縮程式碼

所以打算使用webpack作為新輪子的打包工具。


預期目標

  1. 期望使用es6語法編寫外掛程式碼,程式碼整潔易讀。
  2. 支援模組化程式設計,專案程式碼劃分清晰。
  3. 程式碼合併打包和壓縮自動化
  4. 支援啟動開發環境除錯外掛程式碼
  5. 外掛支援window全域性引用和支援commonjs模組匯入

初始化專案檔案

npm初始化

首先第一步是初始化專案。

輸入npm init命令生成package.json檔案。

配置webpack作為你新輪子的打包工具

建立專案資料夾

接下來是將各個模組的檔案架建立完成。

├─ 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 # 定義生產環境的環境變數
複製程式碼

編寫程式碼

  1. 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')
}

複製程式碼
  1. 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')
})

複製程式碼
  1. 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 啟動開發環境入口
複製程式碼

編寫程式碼

  1. 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'
            }
        ]
    }
}
複製程式碼
  1. 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作為你新輪子的打包工具

編寫構建生產檔案的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 # 裝飾器檔案
複製程式碼
  1. index.js的檔案程式碼

在入口檔案匯入地圖類,並匯出,使之可被外部呼叫。

import MapConstructor from './Map' // 匯入地圖類
export default MapConstructor
複製程式碼
  1. 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);  // 初始化地圖,設定中心點座標和地圖級別
    }
}
複製程式碼
  1. 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目錄下。

配置webpack作為你新輪子的打包工具

如何引用外掛

  1. 使用<script>標籤引入打包後的main.min.js檔案可以直接在window物件下面直接例項化TdrMap類。
<script src="./main.min.js"></script>
複製程式碼
  1. 使用commonjs模組規範引入的指令碼。(我們必須先在package.json檔案下定義main屬性的值為dist/main.min.js,表示這個外掛的入口檔案是main.min.js檔案),然後將你開發完成的包上傳到npm上(如何將包上傳到npm),最後使用npm install xxx安裝,即可在程式碼中引用。
import TdrMap from 'tdr-map'
複製程式碼

測試打包後的生產檔案是否能呼叫成功

最後看到TdrMap類已經可以成功例項化了,外掛的版本資訊也成功列印出來了。

配置webpack作為你新輪子的打包工具

最後

本專案的使用的webpack3.10.0, 如果要照著我的配置寫的話,建議使用和我同樣的版本號,另外其他依賴模組的版本號可以參考我package.json檔案中的devDependencies

專案檔案將做為種子檔案:github

ps: 最近正在找工作,求杭州地區內推,最好是濱江地區,謝謝了~

郵件地址:(845058952@qq.com)
複製程式碼

相關文章