上手webpack4并进阶?来看这里~

webpack作为一个模块打包器,主要用于前端工程中的依赖梳理和模块打包,将我们开发的具有高可读性和可维护性的代码文件打包成浏览器可以识别并正常运行的压缩代码,主要包括样式文件处理成css,各种新式的JavaScript转换成浏览器认识的写法等,也是前端工程师进阶的不二法门,本文借鉴了部分vue-cliwebpack的配置思路,还有一些网上比较好的解决方案,在此对这些作者一并表示感谢。

webpack.config.js配置项简介

  1. Entry:入口文件配置,Webpack 执行构建的第一步将从 Entry 开始,完成整个工程的打包。
  2. Module:模块,在Webpack里一切皆模块,Webpack会从配置的Entry开始递归找出所有依赖的模块,最常用的是rules配置项,功能是匹配对应的后缀,从而针对代码文件完成格式转换和压缩合并等指定的操作。
  3. Loader:模块转换器,用于把模块原内容按照需求转换成新内容,这个是配合Module模块中的rules中的配置项来使用。
  4. Plugins:扩展插件,在Webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。(插件API)
  5. Output:输出结果,在Webpack经过一系列处理并得出最终想要的代码后输出结果,配置项用于指定输出文件夹,默认是./dist
  6. DevServer:用于配置开发过程中使用的本机服务器配置,属于webpack-dev-server这个插件的配置项。

webpack打包流程简介

  • 根据传入的参数模式(development | production)来加载对应的默认配置
  • entry里配置的module开始递归解析entry所依赖的所有module
  • 每一个module都会根据rules的配置项去寻找用到的loader,接受所配置的loader的处理
  • entry中的配置对象为分组,每一个配置入口和其对应的依赖文件最后组成一个代码块文件(chunk)并输出
  • 整个流程中webpack会在恰当的时机执行plugin的逻辑,来完成自定义的插件逻辑

基本的webpack配置搭建

首先通过以下的脚本命令来建立初始化文件:

npm init -y
npm i webpack webpack-cli -D // 针对webpack4的安装
mkdir src && cd src && touch index.html index.js
cd ../ && mkdir dist && mkdir static
touch webpack.config.js
npm i webpack-dev-server --save-dev

修改生成的package.json文件,来引入webpack打包命令:

"scripts": {
    "build": "webpack --mode production",
    "dev": "webpack-dev-server --open --mode development"
}

webpack.config.js文件加入一些基本配置loader,从而基本的webpack4.x的配置成型(以两个页面入口为例):

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin') // 复制静态资源的插件
const CleanWebpackPlugin = require('clean-webpack-plugin') // 清空打包目录的插件
const HtmlWebpackPlugin = require('html-webpack-plugin') // 生成html的插件
const ExtractTextWebapckPlugin = require('extract-text-webpack-plugin') //CSS文件单独提取出来
const webpack = require('webpack')

module.exports = {
    entry: {
        index: path.resolve(__dirname, 'src', 'index.js'),
        page: path.resolve(__dirname, 'src', 'page.js'),
        vendor:'lodash' // 多个页面所需的公共库文件,防止重复打包带入
    },
    output:{
        publicPath: '/',  //这里要放的是静态资源CDN的地址
        path: path.resolve(__dirname,'dist'),
        filename:'[name].[hash].js'
    },
    resolve:{
        extensions: [".js",".css",".json"],
        alias: {} //配置别名可以加快webpack查找模块的速度
    },
    module: {
        // 多个loader是有顺序要求的,从右往左写,因为转换的时候是从右往左转换的
        rules:[
            {
                test: /\.css$/,
                use: ExtractTextWebapckPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'postcss-loader'] // 不再需要style-loader放到html文件内
                }),
                include: path.join(__dirname, 'src'), //限制范围,提高打包速度
                exclude: /node_modules/
            },
            {
                test:/\.less$/,
                use: ExtractTextWebapckPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'postcss-loader', 'less-loader']
                }),
                include: path.join(__dirname, 'src'),
                exclude: /node_modules/
            },
            {
                test:/\.scss$/,
                use: ExtractTextWebapckPlugin.extract({
                    fallback: 'style-loader',
                    use:['css-loader', 'postcss-loader', 'sass-loader']
                }),
                include: path.join(__dirname, 'src'),
                exclude: /node_modules/
            },
            {
                test: /\.jsx?$/,
                use: {
                    loader: 'babel-loader',
                    query: { //同时可以把babel配置写到根目录下的.babelrc中
                      presets: ['env', 'stage-0'] // env转换es6 stage-0转es7
                    }
                }
            },
            { //file-loader 解决css等文件中引入图片路径的问题
            // url-loader 当图片较小的时候会把图片BASE64编码,大于limit参数的时候还是使用file-loader 进行拷贝
                test: /\.(png|jpg|jpeg|gif|svg)/,
                use: {
                  loader: 'url-loader',
                  options: {
                    outputPath: 'images/', // 图片输出的路径
                    limit: 1 * 1024
                  }
                }
            }
        ]
    },
    plugins: [
        // 多入口的html文件用chunks这个参数来区分
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname,'src','index.html'),
            filename:'index.html',
            chunks:['index', 'vendor'],
            hash:true,//防止缓存
            minify:{
                removeAttributeQuotes:true//压缩 去掉引号
            }
        }),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname,'src','page.html'),
            filename:'page.html',
            chunks:['page', 'vendor'],
            hash:true,//防止缓存
            minify:{
                removeAttributeQuotes:true//压缩 去掉引号
            }
        }),
        new webpack.ProvidePlugin({
            _:'lodash' //所有页面都会引入 _ 这个变量,不用再import引入
        }),
        new ExtractTextWebapckPlugin('css/[name].[hash].css'), // 其实这个特性只用于打包生产环境,测试环境这样设置会影响HMR
        new CopyWebpackPlugin([
            {
                from: path.resolve(__dirname, 'static'),
                to: path.resolve(__dirname, 'dist/static'),
                ignore: ['.*']
            }
        ]),
        new CleanWebpackPlugin([path.join(__dirname, 'dist')]),
    ],
    devtool: 'eval-source-map', // 指定加source-map的方式
    devServer: {
        contentBase: path.join(__dirname, "dist"), //静态文件根目录
        port: 3824, // 端口
        host: 'localhost',
        overlay: true,
        compress: false // 服务器返回浏览器的时候是否启动gzip压缩
    },
    watch: true, // 开启监听文件更改,自动刷新
    watchOptions: {
        ignored: /node_modules/, //忽略不用监听变更的目录
        aggregateTimeout: 500, //防止重复保存频繁重新编译,500毫米内重复保存不打包
        poll:1000 //每秒询问的文件变更的次数
    },
}

在命令行下用以下命令安装loader和依赖的插件,生成完全的package.json项目依赖树。

npm install extract-text-webpack-plugin@next --save-dev
npm i style-loader css-loader postcss-loader --save-dev
npm i less less-loader --save-dev
npm i node-sass sass-loader --save-dev
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 --save-dev
npm i file-loader url-loader --save-dev

npm i html-webpack-plugin ---save-dev
npm i clean-webpack-plugin --save-dev
npm i copy-webpack-plugin --save-dev

npm run dev

默认打开的页面是index.html页面,可以加上/page.html来打开page页面看效果。
PS: 关于loader的详细说明可以参考webpack3.x的学习介绍,上面配置中需要注意的是多页面的公共库的引入采用的是vendor+暴露全局变量的方式,其实这种方式有诸多弊端,而webpack4针对这种情况设置了新的API,有兴趣的话,就继续看下面的高级配置吧。

进阶的webpack4配置搭建

包含以下几个方面:

  1. 针对CSSJSTreeShaking来减少无用代码,针对JS需要对已有的uglifyjs进行一些自定义的配置(生产环境配置)
  2. 新的公共代码抽取工具(optimization.SplitChunksPlugin)提取重用代码,减小打包文件。(代替commonchunkplugin,生产和开发环境都需要)
  3. 使用HappyPack进行javascript的多进程打包操作,提升打包速度,并增加打包时间显示。(生产和开发环境都需要)
  4. 创建一个webpack.dll.config.js文件打包常用类库到dll中,使得开发过程中基础模块不会重复打包,而是去动态连接库里获取,代替上一节使用的vendor。(注意这个是在开发环境使用,生产环境打包对时间要求并不高,后者往往是项目持续集成的一部分)
  5. 模块热替换,还需要在项目中增加一些配置,不过大型框架把这块都封装好了。(开发环境配置)
  6. webpack3新增的作用域提升会默认在production模式下启用,不用特别配置,但只有在使用ES6模块才能生效。

关于第四点,需要在package.json中的script中增加脚本:
"build:dll": "webpack --config webpack.dll.config.js --mode development",

补充安装插件的命令行:

npm i purify-css purifycss-webpack -D // 用于css的tree-shaking
npm i webpack-parallel-uglify-plugin -D // 用于js的tree-shaking
npm i happypack@next -D //用于多进程打包js
npm i progress-bar-webpack-plugin -D //用于显示打包时间和进程
npm i webpack-merge -D //优化配置代码的工具
npm i optimize-css-assets-webpack-plugin -D //压缩CSS
npm i chalk -D
npm install css-hot-loader -D // css热更新
npm i mini-css-extract-plugin -D

TreeShaking需要增加的配置代码,这一块参考webpack文档,需要三方面因素,分别是:

  • 使用ES6模块(import/export)
  • package.json文件中声明sideEffects指定可以treeShaking的模块
  • 启用UglifyJSPlugin,多入口下用WebpackParallelUglifyPlugin(这是下面的配置代码做的事情)
/*最上面要增加的声明变量*/
const glob = require('glob')
const PurifyCSSPlugin = require('purifycss-webpack')
const WebpackParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

/*在`plugins`配置项中需要增加的两个插件设置*/
new PurifyCSSPlugin({
    paths: glob.sync(path.join(__dirname, 'src/*.html'))
}),
new WebpackParallelUglifyPlugin({
    uglifyJS: {
        output: {
            beautify: false, //不需要格式化
            comments: false //不保留注释
        },
        compress: {
            warnings: false, // 在UglifyJs删除没有用到的代码时不输出警告
            drop_console: true, // 删除所有的 `console` 语句,可以兼容ie浏览器
            collapse_vars: true, // 内嵌定义了但是只用到一次的变量
            reduce_vars: true // 提取出出现多次但是没有定义成变量去引用的静态值
        }
    }
    // 有兴趣可以探究一下使用uglifyES
}),

关于ES6模块这个事情,上文的第六点也提到了只有ES6模块写法才能用上最新的作用域提升的特性,首先webpack4.x并不需要额外修改babelrc的配置来实现去除无用代码,这是从webpack2.x升级后支持的,改用sideEffect声明来实现。但作用域提升仍然需要把babel配置中的module转换去掉,修改后的.babelrc代码如下:

{
  "presets": [["env", {"loose": true, "modules": false}], "stage-0"]
}

但这个时候会发现import引入样式文件就被去掉了……只能使用require来改写了。

打包DLL第三方类库的配置项,用于开发环境:

  1. webpack.dll.config.js配置文件具体内容:
const path = require('path')
const webpack = require('webpack')
/**
 * 尽量减小搜索范围
 * target: '_dll_[name]' 指定导出变量名字
 */
module.exports = {
    entry: {
        vendor: ['jquery', 'lodash']
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].dll.js',
        library: '_dll_[name]' // 全局变量名,其他模块会从此变量上获取里面模块
    },
    // manifest是描述文件
    plugins: [
        new webpack.DllPlugin({
            name: '_dll_[name]',
            path: path.join(__dirname, 'dist', 'manifest.json')
        })
    ]
}
  1. webpack.config.js中增加的配置项:
/*找到上一步生成的`manifest.json`文件配置到`plugins`里面*/
new webpack.DllReferencePlugin({
    manifest: require(path.join(__dirname, '..', 'dist', 'manifest.json')),
}),

多文件入口的公用代码提取插件配置:

/*webpack4.x的最新优化配置项,用于提取公共代码,跟`entry`是同一层级*/
optimization: {
    splitChunks: {
        cacheGroups: {
            commons: {
                chunks: "initial",
                name: "common",
                minChunks: 2,
                maxInitialRequests: 5,
                minSize: 0
            }
        }
    }
}

/*针对生成HTML的插件,需增加common,也去掉上一节加的vendor*/
new HtmlWebpackPlugin({
    template: path.resolve(__dirname,'src','index.html'),
    filename:'index.html',
    chunks:['index', 'common'],
    vendor: './vendor.dll.js', //与dll配置文件中output.fileName对齐
    hash:true,//防止缓存
    minify:{
        removeAttributeQuotes:true//压缩 去掉引号
    }
}),
new HtmlWebpackPlugin({
    template: path.resolve(__dirname,'src','page.html'),
    filename:'page.html',
    chunks:['page', 'common'],
    vendor: './vendor.dll.js', //与dll配置文件中output.fileName对齐
    hash:true,//防止缓存
    minify:{
        removeAttributeQuotes:true//压缩 去掉引号
    }
}),

PS: 这一块要多注意,对应入口的HTML文件也要处理,关键是自定义的vendor项,在开发环境中引入到html

HappyPack的多进程打包处理:

/*最上面要增加的声明变量*/
const HappyPack = require('happypack')
const os = require('os') //获取电脑的处理器有几个核心,作为配置传入
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const ProgressBarPlugin = require('progress-bar-webpack-plugin')

/*在`module.rules`配置项中需要更改的`loader`设置*/
{
    test: /\.jsx?$/,
    loader: 'happypack/loader?id=happy-babel-js',
    include: [path.resolve('src')],
    exclude: /node_modules/,
},

/*在`plugins`配置项中需要增加的插件设置*/
new HappyPack({ //开启多线程打包
    id: 'happy-babel-js',
    loaders: ['babel-loader?cacheDirectory=true'],
    threadPool: happyThreadPool
}),
new ProgressBarPlugin({
    format: '  build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)'
})

PS:要记住这种使用方法下一定要在根目录下加.babelrc文件来设置babel的打包配置。

开发环境的代码热更新:
其实针对热刷新,还有两个方面要提及,一个是html文件里面写代码的热跟新(这个对于框架不需要,如果要实现,建议使用glup,后面有代码),一个是写的样式代码的热更新,这两部分也要加进去。让我们一起看看热更新需要增加的配置代码:

/*在`devServer`配置项中需增加的设置*/
hot:true

/*在`plugins`配置项中需要增加的插件设置*/
new webpack.HotModuleReplacementPlugin(), //模块热更新
new webpack.NamedModulesPlugin(), //模块热更新

在业务代码中要做一些改动,一个比较low的例子为:

if(module.hot) { //设置消息监听,重新执行函数
    module.hot.accept('./hello.js', function() {
        div.innerHTML = hello()
    })
}

但还是不能实现在html修改后自动刷新页面,这里有个概念是热更新不是针对页面级别的修改,这个问题有一些解决方法,但目前都不是很完美,可以参考这里,现在针对CSS的热重载有一套解决方案如下,需要放弃使用上文提到的ExtractTextWebapckPlugin,引入mini-css-extract-pluginhot-css-loader来实现,前者在webpack4.x上与hot-css-loader有报错,让我们改造一番:

/*最上面要增加的声明变量*/
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

/*在样式的`loader`配置项中需增加的设置,实现css热更新,以css为例,其他可以参照我的仓库来写*/
{
    test: /\.css$/,
    use: ['css-hot-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
    include: [resolve('src')], //限制范围,提高打包速度
    exclude: /node_modules/
}

/*在`plugins`配置项中需要增加的插件设置,注意这里不能写[hash],否则无法实现热跟新,如果有hash需要,可以开发环境和生产环境分开配置*/
new MiniCssExtractPlugin({
    filename: "[name].css",
    chunkFilename: "[id].css"
})

用于生产环境压缩css的插件,看官方文档说明,样式文件压缩没有内置的,所以暂时引用第三方插件来做,以下是配置示例。

/*要增加的声明变量*/
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

/*在`plugins`配置项中需要增加的插件设置*/
new OptimizeCSSPlugin({
    cssProcessorOptions: {safe: true}
})

最终成果

在进阶部分我们对webpack配置文件根据开发环境和生产环境的不同做了分别的配置,因此有必要分成两个文件,然后发现重复的配置代码很多,作为有代码洁癖的人不能忍,果断引入webpack-merge,来把相同的配置抽出来,放到build/webpack.base.js中,而后在build/webpack.dev.config.js(开发环境)和build/webpack.prod.config.js(生产环境)中分别引用,在这个过程中也要更改之前文件的路径设置,以免打包或者找文件的路径出错,同时将package.json中的脚本命令修改为:

"scripts": {
    "build": "webpack --config build/webpack.prod.config.js --mode production",
    "dev": "webpack-dev-server --open --mode development --config build/webpack.dev.config.js",
    "dev:dll": "webpack --config build/webpack.dll.config.js --mode development",
    "start": "npm run dev:dll && npm run dev"
}

接下来就是代码的重构过程,这个过程其实我建议大家自己动手做一做,就能对webpack配置文件结构更加清晰,下面的代码过于冗长,有兴趣请到我的github地址来看。

  • build文件夹下的webpack.base.js文件(太长不上)
  • build文件夹下的webpack.dev.config.js文件(太长不上)
  • build文件夹下的webpack.prod.config.js文件(太长不上)

多说一句,就是实现JS打包的treeShaking还有一种方法是编译期分析依赖,利用uglifyjs来完成,这种情况需要保留ES6模块才能实现,因此在使用这一特性的仓库中,.babelrc文件的配置为:"presets": [["env", { "modules": false }], "stage-0"],就是打包的时候不要转换模块引入方式的含义。

接下来就可以运行npm start,看一下进阶配置后的成果啦,吼吼,之后只要不进行build打包操作,通过npm run dev启动,不用重复打包vendor啦。生产环境打包使用的是npm run build

以上就是对webpack4.x配置的踩坑过程,期间参考了大量谷歌英文资料,希望能帮助大家更好地掌握wepback最新版本的配置,以上内容亲测跑通,有问题的话,欢迎加我微信(kashao3824)讨论,来github地址issue也可,欢迎fork/star

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容