webpack实战(一):真实项目中一个完整的webpack配置

4,148 阅读8分钟

字数:3700

阅读时间:15分钟

环境:webpack3.8.1

前言

前段时间,使用webpack查阅资料时发现要么是入门级文章,要么是如何优化打包速度或如何使用某个plugin、loader的文章。找不到一个在真实项目中的使用webpack的完整方案用以参考,所以花了许多精力去整合资料、查看代码和踩坑。

因此我将自己摸索的一个配置方案,分享出来,希望能提供一点借鉴。不足之处,欢迎大伙指正。

说明一下,本文旨在讲述思路和遇到的问题,不会涉及到基础的讲解。如果想了解基础,这里可以给大伙推荐两个非常好的入门资料:

入门Webpack,看这篇就够了:初步地了解webpack的用法已经简单地练手。

webpack中文文档:这一版的官方文档,相对之前大家诟病已久的文档混乱问题有了很大的改善,最好不过的学习资料了。

正文

为增加代码的可读性和维护性,我将配置拆分为以下五个配置文件:

webpack.common.config.js 公共配置
webpack.dev.config.js 开发环境配置
webpack.prod.config.js 生产环境配置
webpack.dll.config.js 公共库配置
webpack.alias.js 模块地址配置

为提升打包效率,我会将一些变化较小的代码和第三方库都打包成公共库,webpack.dll.config.js就是打包公共库的配置文件,如果其中的内容没有变化,之后的打包不会再处理这些文件了,极大地增加了打包效率。如果使用了较多第三方库,强烈建议使用这种方式打包。

因为开发环境配置和生产环境配置有许多差异,因此分别做配置,分别对应着webpack.dev.config.jswebpack.prod.config.js配置文件。然后提取其中的公共部分,这就是我们的公共配置文件webpack.common.config.js

最后,笔者对每一个模块都做了别名配置,以解耦代码对代码目录的依赖,对应着我们的webpack.alias.js配置文件。

下面,我们就一起探讨一下这五个配置文件的具体内容。

1.webpack.common.config.js

公共配置,先上代码:

const wepackMerge = require('webpack-merge');
const Path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
const GTLoaderFilesPlugin = require('./plugins/gt-file-loader-plugin');

const ProdConfig = require('./webpack.prod.config');
const DevConfig = require('./webpack.dev.config');
const alias = require('./webpack.alias');
const dlls = require('./webpack.dll.config');

//根据条件处理相关配置
const genarateConfig = env => {
    //样式loader
    let cssLoader = [{
        loader: 'css-loader',
        options: {
            sourceMap: true
        }
    }, {
        loader: 'postcss-loader',
        options: {
            ident: 'postcss',
            plugins: [
                require('postcss-cssnext')()
            ],
            sourceMap: true
        }
    }, {
        loader: 'less-loader',
        options: {
            sourceMap: true
        }
    }];
    let styleLoader = [{
        test: /\.(css|less)$/,
        use: env === 'prod' ? ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: cssLoader
        }) : [{
            loader: 'style-loader',
            options: {
                sourceMap: true
            }
        }].concat(cssLoader)
    }];

    //脚本loader
    let jsLoader = [{
        test: /\.js$/,
        exclude: /(node_modules|bower_components|libs)/,
        use: [{
            loader: 'babel-loader'
        }].concat(env === 'dev' ? [{
            loader: 'eslint-loader'
        }] : [])
    }];

    //文件处理loader
    let fileLoaderOptions = {
        useRelativePath: false,
        name: '[name]-[hash:5].[ext]'
    };
    if (env === 'prod') {
        fileLoaderOptions.limit = 10000;
    }
    let fileLoader = [{
        test: /\.(jpg|jpeg|png|icon)$/,
        use: [{
            loader: env === 'dev' ? 'file-loader' : 'url-loader',
            options: env === 'dev' ? fileLoaderOptions : Object.assign({}, fileLoaderOptions, {
                outputPath: '../dist/img'
            })
        }]
    }, {
        //解析字体文件
        test: /\.(eot|svg|ttf|woff2?)$/,
        use: [{
            loader: env === 'dev' ? 'file-loader' : 'url-loader',
            options: env === 'dev' ? fileLoaderOptions : Object.assign({}, fileLoaderOptions, {
                outputPath: '../dist/fonts'
            })
        }]
    }, {
        //解析主页面和页面上的图片
        test: /\.html$/,
        exclude: /(node_modules|bower_components)/,
        use: {
            loader: 'html-loader',
            options: {
                attrs: ['img:src', 'img:data-src'],
                minimize: true
            }
        }
    }];

    //webpack插件
    let plugins = [];

    //组织第三方库插件
    for (let key in dlls.entry) {
        //组织DllReferencePlugin
        let dllPlugin = new Webpack.DllReferencePlugin({
            manifest: require('../dll/manifest/' + key + '.manifest.json')
        });
        plugins.push(dllPlugin);
    }

    //加载js
    plugins.push(new AddAssetHtmlPlugin({
        filepath: Path.join(__dirname, '../dll/*.js'),
        hash: true,
        includeSourcemap: false,
        publicPath: './dll/',
        outputPath: '../dist/dll/'
    }));

    //加载css
    plugins.push(new AddAssetHtmlPlugin({
        filepath: Path.join(__dirname, '../dll/*.css'),
        hash: true,
        typeOfAsset: 'css',
        includeSourcemap: false,
        publicPath: './dll/',
        outputPath: '../dist/dll/'
    }));

    //入口html插件
    plugins.push(new HtmlWebpackPlugin({
        template: Path.join(__dirname, '../src/control.html'),
        filename: 'index.html',
        inject: true,
        chunks: ['vendor', 'example']
    }));

    //拷贝文件
    plugins.push(new CopyWebpackPlugin([{
        // 第三方的字体文件
        from: './dll/fonts',
        to: '../dist/fonts'
    }, {
        //表单页面文件
        from: './src/form/core/views',
        to: '../dist/core-views'
    }, {
        //表单页面文件
        from: './src/form/office/views',
        to: '../dist/office-views'
    }], {
        ignore: ['**/.svn/**']
    }));

    //友好提示插件
    plugins.push(new FriendlyErrorsPlugin());

    //不打包默认加载项
    plugins.push(new Webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));

    //将加载项写入loader.js中
    plugins.push(new GTLoaderFilesPlugin());

    let config = {
        devtool: 'source-map',
        output: {
            path: Path.join(__dirname, '../dist/'),
            filename: env === 'dev' ? '[name]-[hash:5].bundle.js' : '[name]-[chunkhash:5].bundle.js'
        },
        module: {
            rules: [].concat(styleLoader).concat(jsLoader).concat(fileLoader)
        },
        plugins: plugins,
        resolve: {
            alias: alias
        }
    };

    return config;
};

module.exports = env => {
    let config = env === 'dev' ? DevConfig : ProdConfig;
    let result = wepackMerge(genarateConfig(env), config);
    return result;
};

入口

开发环境和生产环境皆使用这个配置文件执行webpack,通过执行CLI命令时传入的环境变量来区分。开发环境传入dev,生产环境传入prod,借助于npm的scripts命令,可更便利地实现,代码如下:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --env prod --config build/webpack.common.config.js",
    "server": "webpack-dev-server --env dev --config build/webpack.common.config.js --open",
    "dll": "webpack --config build/webpack.dll.config.js"
  }

运行npm run server就可以打开开发环境了。

公共配置中,生成配置代码如下:

module.exports = env => {
    let config = env === 'dev' ? DevConfig : ProdConfig;
    let result = wepackMerge(genarateConfig(env), config);
    return result;
};

使用webpack-merge插件,根据传入的参数将不同的配置与公共配置进行融合。注意,由于loader是倒序执行的,所以loader相关的配置无法使用这个方式融合,只能在代码中自行处理。这里genarateConfig就是处理公共配置的函数。

样式

生产环境:less-loader→postcss-loader→css-loader→extract-text-webpack-plugin

开发环境:less-loader→postcss-loader→css-loader→style-loader

less-loader:webpack自带loader,预编译less文件,将less语法编译成css语法。

postcss-loader:webpack自带loader,css转换工具,自动添加浏览器前缀、压缩css、支持未来语法。

css-loader:webpack自带loader,编译css。

extract-text-webpack-plugin:webpack自带插件,extract()函数会返回一个loader,输出独立样式文件,一般用于生产环境。

style-loader:webpack自带loader,将样式以style标签的方式直接插入到html文档中。

注意: postcss-loader中引入postcss-cssnext模块,可以支持未来语法。所有loader设置sourceMap: true才能在控制台看到样式源码。

脚本

生产环境:babel-loader

开发环境:eslint-loader→babel-loader

eslint-loader:webpack自带loader,需要依赖Eslint工具,做静态代码检查只用。可以配合webpack-dev-server的overlay使用。

babel-loader:编译js,兼容新特性。

文件

生产环境:html-loader→url-loader

开发环境:html-loader→file-loader

html-loader:webpack自带loader,编译html文件,将其中加载的图片等资源作为模块处理。

url-loader:webpack自带loader,解析图片、字体等文件资源,可以将超过限制的资源解析成base64编码字符串,达到减少请求数的优化效果。

file-loader:webpack自带loader,同url-loader,只是不会将文件解析成base64编码字符串。

插件

DllReferencePlugin:webpack自带插件,配合公共库webpack.dll.config.js使用。

add-asset-html-webpack-plugin:在生成的html页面中自动插入资源,这里使用它引入了公共库中的js和css资源。

html-webpack-plugin:根据模板生成html文件,默认支持ejs模板语法。需要注意:与html-loader共用时,由于html-loader会先将html文件编译成字符串,从而导致ejs语法解析失效。我使用的解决方案如下:所有使用到ejs语法的后缀改为.ejs,其中加载的图片等资源文件手动加载模块。例:<img src="${require('./assets/img/6.jpg')}" alt="">。然后html-loader不解析以ejs为后缀的文件。

copy-webpack-plugin:webpack自带插件,用以复制文件,主要复制不作为模块引入的资源文件,例如:一些图片字体等文件,没必要编译,直接复制过来打包速度更快。

friendly-errors-webpack-plugin:友好提示插件,CLI中提示信息可视化更加友好。如果使用 git bash 或者mac 的 Terminal 则没必要安装该插件。

IgnorePlugin:webpack自带插件,不打包默认加载项,webpack会默认打包locale、moment等模块,如果项目不需要,可以使用该插件屏蔽。

GTLoaderFilesPlugin:这是我自定义的资源加载插件,可忽略。

主配置

context:配置entry和loader的参考路径。

resolve.alias:模块别名配置,配合webpack.alias.js使用

2.webpack.dev.config.js

开发环境配置,先上代码:

const Webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    entry: {
        example: './examples/index.js'
    },
    devServer: {
        port: '9091',
        overlay: true,
        //设置为false则会在页面中显示当前webpack的状态
        inline: true,
        historyApiFallback: true,
        //代理配置
        proxy: {
        },
        hot: true
        //强制页面不通过刷新页面更新文件
        // hotOnly: true
    },
    plugins: [
        //分析插件
        // new BundleAnalyzerPlugin(),
        //模块热更新插件
        new Webpack.HotModuleReplacementPlugin(),
        //使用HMR时显示模块的相对路径
        new Webpack.NamedModulesPlugin()
    ]
};

devServer:配置webpack自带的服务器,以作调试只用。需要安装webpack-dev-server插件,注意,只能安装V3之前的版本,V3版本是兼容webpack4的,无法在webpack3中使用。

webpack-bundle-analyzer:第三方的分析插件,可以对打包结果进行分析。也可以使用官方的分析方案:结合插件stats-webpack-plugin生成的分析结果文件和官方提供的在线工具官方工具来分析打包结果。

HotModuleReplacementPlugin:webpack自带工具,模块热更新必须插件。

NamedModulesPlugin:webpack自带插件,用模块的路径命名模块,运行结果更清晰。不使用这个插件,webpack就会默认使用一个随机ID命名。利于调试,官方推荐开发环境需要使用的插件。

3.webpack.prod.config.js

生产环境配置,先上代码:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const Webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ZipPlugin = require('zip-webpack-plugin');
const StatsPlugin = require('stats-webpack-plugin');
const SvnInfo = require('svn-info').sync('https://218.106.122.66/svn/framework/trunk/gt-ui', 'HEAD');

const Path = require('path');
const pkg = require('../package.json');

module.exports = {
    entry: {
        frame0: 'frame',
        frame2: 'frame2',
        frame3: 'frame3',
        center1: 'center1',
        center2: 'center2',
        center3: 'center3',
        login1: 'login1',
        login2: 'login2',
        form: 'form',
        example: './examples/index.js'
    },
    plugins: [
        //模块分析页面
        // new BundleAnalyzerPlugin(),
        new Webpack.optimize.CommonsChunkPlugin({
            names: ['vendor'],
            minChunks: 2
        }),
        //混淆代码
        new UglifyJsPlugin({
            sourceMap: true,
            //多线程处理
            parallel: true,
            //使用缓存
            cache: true
        }),
        //提取css文件
        new ExtractTextPlugin({
            filename: '[name]-[hash:5].css'
        }),
        new CleanWebpackPlugin(['dist', 'package'], {
            root: Path.join(__dirname, '../')
        }),
        new Webpack.NamedChunksPlugin(),
        new Webpack.NamedModulesPlugin(),
        //版本信息
        new Webpack.BannerPlugin({
            banner: `Name: ${pkg.name}\nSVNVersion: ${SvnInfo.revision}\nDate: ${new Date().toISOString().slice(0, 10)}\nDescription: ${pkg.description}`,
            raw: false,
            entryOnly: true,
            include: /\.js/g
        }),
        //分析结果
        new StatsPlugin('../stats.json', {
            chunkModules: true,
            exclude: [/node_modules/]
        }),
        //复制文档页面
        new CopyWebpackPlugin([{
            // 第三方的字体文件
            from: './examples',
            to: '../dist/examples'
        }, {
            //表单页面文件
            from: './docs',
            to: '../dist/docs'
        }], {
            ignore: ['**/.svn/**']
        }),
        //打包生成包的主页
        new HtmlWebpackPlugin({
            template: Path.join(__dirname, '../src/index.html'),
            filename: '../index.html',
            inject: true
        }),
        //压缩文件夹
        new ZipPlugin({
            filename: 'gt-ui.zip',
            path: '../package/',
            pathPrefix: 'dist'
        })
    ],
    profile: true
};

CommonsChunkPlugin:webpack自带插件,提取多个入口中公共代码,webpack最开始的核心优势codesplting的实现之一。

uglifyjs-webpack-plugin:代码压缩混淆插件,开启多线程和缓存可以加快打包速度。

clean-webpack-plugin:清空文件夹插件,每次打包前先清空之前打包残留文件。

NamedChunksPlugin:webpack自带插件,开发环境配置中有说过,这里旨在长效缓存之用。如果不使用这个插件,webpack生成的随机ID会导致最终生成的代码文件对应的hash值变化,导致长效缓存失效。

NamedModulesPlugin:同上,这里作用于chunk依赖的模块。

BannerPlugin:webpack自带插件,在代码中添加代码段,如代码版本、版权等信息。

svn-info:获取当前代码的SVN信息。

stats-webpack-plugin:生成打包分析文件,以在官方提供的在线析工具上使用。

zip-webpack-plugin:将打包结果压缩成压缩包。在使用这个插件时,遇到了该插件的执行顺序错误,导致打包失败的问题。相同的配置,有时该插件会先于其他插件执行,出现需要压缩的文件还没生成导致打包中断问题,有时却不会。查了一些资料,发现除非是插件本身处理了执行顺序的问题,否则webpack的插件的执行顺序其实是不定的(略坑,相比而言gulp就要确定地多)。这里有一个替代插件,filemanager-webpack-plugin

4.webpack.dll.config.js

公共库配置,先上代码:

const Path = require('path');
const Webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const alias = require('./webpack.alias');

module.exports = {
    entry: {
        ngs: ['angular', 'angular-resource', 'angular-sanitize', '@uirouter/angularjs',
            'angular-animate', 'angular-touch', 'angular-cookies'
        ],
        ngui: ['jquery', 'sweetalert', 'datetimepickerCN', 'datetimepicker', 'angular-loading-bar', 'angular-strap', 'angular-ui-grid', 'ui-select',
            'angular-ui-tour', 'angular-ui-tree', 'angular-validation', 'angular-carousel'
        ],
        base: ['babel-polyfill', 'lodash']
    },
    output: {
        path: Path.join(__dirname, '../dll'),
        filename: '[name].dll.js',
        library: '[name]'
    },
    resolve: {
        alias: alias
    },
    plugins: [
        new Webpack.DllPlugin({
            path: Path.join(__dirname, '../dll/manifest/', '[name].manifest.json'),
            name: '[name]'
        }),
        new CopyWebpackPlugin([{
            from: './src/libs/bootstrap-datetimepicker-master/css/bootstrap-datetimepicker.min.css'
        }, {
            from: './node_modules/angular-loading-bar/build/loading-bar.css'
        }, {
            from: './node_modules/ui-select/dist/select.css'
        }, {
            from: './node_modules/angular-ui-tree/dist/angular-ui-tree.min.css'
        }, {
            from: './node_modules/angular-carousel/dist/angular-carousel.min.css'
        }])
    ]
};

这里,为了加快打包速度,我将一些不需要编译的文件直接用拷贝插件拷贝,用加载到代码中。

DllPlugin :webpack自带插件,生成库定义文件的,配合DllReferencePlugin使用。

5.webpack.alias.js

模块别名配置,先上代码:

var Path = require('path');

module.exports = {
    api: Path.join(__dirname, '../src/common/api'),
    //自定义控件
    ngControl: Path.join(__dirname, '../src/custom/controls/control'),
    //框架
    frame: Path.join(__dirname, '../src/custom/frame/frame'),
    frame1: Path.join(__dirname, '../src/custom/frame/frame1/frame'),
    frame2: Path.join(__dirname, '../src/custom/frame/frame2/frame'),
    frame3: Path.join(__dirname, '../src/custom/frame/frame3/frame'),
    login1: Path.join(__dirname, '../src/custom/login/login1/login'),
    login2: Path.join(__dirname, '../src/custom/login/login2/login'),
    center1: Path.join(__dirname, '../src/custom/system-center/center1/system-center'),
    center2: Path.join(__dirname, '../src/custom/system-center/center2/system-center'),
    center3: Path.join(__dirname, '../src/custom/system-center/center3/system-center'),
    frameManager: Path.join(__dirname, '../src/custom/frame-manager')
};

这里面就是配置不同模块对应的具体文件地址,以便维护。

我们的构建配置是分为两大类的:框架配置和应用系统配置。以上是前端框架的构建配置,应用系统的构建配置我在下一篇文章和大伙分享!

*欢迎关注我的微信公众号:*