配置webpack作为你新轮子的打包工具

2,788 阅读9分钟

背景

前两天帮前同事写一个兼容公安内网,外网,专网的多地图合一的地图类库,但是越写越烦躁,整理一下有以下几个痛点:

  1. 使用es5语法编写javascript,语法啰嗦冗长
  2. js代码全部写到一个文件中,没有模块化,项目难以维护
  3. 需要手动使用压缩工具压缩代码

所以打算使用webpack作为新轮子的打包工具。


预期目标

  1. 期望使用es6语法编写插件代码,代码整洁易读。
  2. 支持模块化编程,项目代码划分清晰。
  3. 代码合并打包和压缩自动化
  4. 支持启动开发环境调试插件代码
  5. 插件支持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 # 定义生产环境的环境变量

编写代码

  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配置代码

创建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目录下。

如何引用插件

  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类已经可以成功实例化了,插件的版本信息也成功打印出来了。

最后

本项目的使用的webpack3.10.0, 如果要照着我的配置写的话,建议使用和我同样的版本号,另外其他依赖模块的版本号可以参考我package.json文件中的devDependencies

项目文件将做为种子文件:github

ps: 最近正在找工作,求杭州地区内推,最好是滨江地区,谢谢了~

邮件地址:(845058952@qq.com)