一、前言

2018/2/25,webpack4正式发布,距离现在已经过去三个多月了,也逐渐趋于稳定,而且现在的最新版本都到了4.12.0(版本迭代快得真是让人害怕)。

很多人都说webpack复杂,难以理解,很大一部分原因是webpack是基于配置的,可配置项很多,并且每个参数传入的形式多种多样(可以是字符串、数组、对象、函数。。。),文档介绍也比较模糊,这么多的配置项各种排列组合,想想都复杂。而gulp基于流的方式来处理文件,无论从理解上,还是功能上都很容易上手。

//gulp
gulp.src('./src/js/**/*.js')
.pipe('babel')
.pipe('uglifyjs')
.dest('./dist/js') //webpack
module.exports = {
entry: './src/main.js',
output: __dirname + '/dist/app.js',
module: {
rules: [{
test: /\.js$/,
loader: 'babel-loader'
}]
},
plugins: [
new require('uglifyjs-webpack-plugin')()
]
}

上面简单对比了webpack与gulp配置的区别,当然这样比较是有问题的,gulp并不能进行模块化的处理。这里主要是想告诉大家使用gulp的时候,我们能明确的知道js文件是先进行babel转译,然后进行压缩混淆,最后输出文件。而webpack对我们来说完全是个黑盒,完全不知道plugins的执行顺序。正是因为这些原因,我们常常在使用webpack时有一些不安,不知道这个配置到底有没有生效,我要按某种方式打包到底该如何配置?

为了解决上面的问题,webpack4引入了零配置的概念(Parcel ???),实际体验下来还是要写不少配置。
但是这不是重点,重点是官方宣传webpack4能够提升构建速度60%-98%,真的让人心动。

二、到底怎么升级

0、初始化配置

首先安装最新版的webpack和webpack-dev-server,然后再安装webpack-cli。webpack4将命令行相关的操作抽离到了webpack-cli中,所以,要使用webpack4,必须安装webpack-cli。当然,如果你不想使用webpack-cli,社区也有替代方案webpack-command,虽然它与webpack-cli区别不大,但是还是建议使用官方推荐的webpack-cli。

npm i webpack@4 webpack-dev-server@3 --save-dev
npm i webpack-cli --save-dev

webpack-cli除了能在命令行接受参数运行webpack外,还具备migrateinit功能。

  1. migrate用来升级webpack配置,能将webpack1的api升级到webpack2,现在用处不大。
$ webpack-cli migrate ./webpack.config.js
✔ Reading webpack config
✔ Migrating config from v1 to v2
- loaders: [
+ rules: [
- loader: 'babel',
- query: {
+ use: [{
+ loader: 'babel-loader'
+ }],
+ options: {
- loader: ExtractTextPlugin.extract('style', 'css!sass')
+ use: ExtractTextPlugin.extract({
+ fallback: 'style',
+ use: 'css!sass'
+ })
? Are you sure these changes are fine? Yes ✔︎ New webpack v2 config file is at /home/webpack-cli/build/webpack.config.js
  1. init可以快速生成一个webpack配置文件的模版,不过用处也不大,毕竟现在的脚手架都集成了webpack的配置。
webpack-cli init

1. Will your application have multiple bundles? No // 如果是多入口应用,可以传入一个object
2. Which module will be the first to enter the application? [example: './src/index'] ./src/index // 程序入口
3. What is the location of "app"? [example: "./src/app"] './src/app'
4. Which folder will your generated bundles be in? [default: dist]
5. Are you going to use this in production? No
6. Will you be using ES2015? Yes //是否使用ES6语法,自动添加babel-loader
7. Will you use one of the below CSS solutions? SASS // 根据选择的样式类型,自动生成 loader 配置
8. If you want to bundle your CSS files, what will you name the bundle? (press enter to skip)
9. Name your 'webpack.[name].js?' [default: 'config']: // webpack.config.js Congratulations! Your new webpack configuration file has been created!

更详细介绍请查看webpack-cli的文档

1、零配置

零配置就意味着webpack4具有默认配置,webpack运行时,会根据mode的值采取不同的默认配置。如果你没有给webpack传入mode,会抛出错误,并提示我们如果要使用webpack就需要设置一个mode。

The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concep...

mode一共有如下三种配置:

  1. none

    这个配置的意思就是不使用任何默认配置

  2. development,开发环境下的默认配置
module.exports = {
//开发环境下默认启用cache,在内存中对已经构建的部分进行缓存
//避免其他模块修改,但是该模块未修改时候,重新构建,能够更快的进行增量构建
//属于空间换时间的做法
cache: true,
output: {
pathinfo: true //输入代码添加额外的路径注释,提高代码可读性
},
devtools: "eval", //sourceMap为eval类型
plugins: [
//默认添加NODE_ENV为development
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
],
optimization: {
namedModules: true, //取代插件中的 new webpack.NamedModulesPlugin()
namedChunks: true
}
}
  1. production,生产环境下的默认配置
module.exports = {
performance: {
hints: 'warning',
maxAssetSize: 250000, //单文件超过250k,命令行告警
maxEntrypointSize: 250000, //首次加载文件总和超过250k,命令行告警
}
plugins: [
//默认添加NODE_ENV为production
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
],
optimization: {
minimize: true, //取代 new UglifyJsPlugin(/* ... */)
providedExports: true,
usedExports: true,
//识别package.json中的sideEffects以剔除无用的模块,用来做tree-shake
//依赖于optimization.providedExports和optimization.usedExports
sideEffects: true,
//取代 new webpack.optimize.ModuleConcatenationPlugin()
concatenateModules: true,
//取代 new webpack.NoEmitOnErrorsPlugin(),编译错误时不打印输出资源。
noEmitOnErrors: true
}
}

其他的一些默认值:

module.exports = {
context: process.cwd()
entry: './src',
output: {
path: 'dist',
filename: '[name].js'
},
rules: [
{
type: "javascript/auto",
resolve: {}
},
{
test: /\.mjs$/i,
type: "javascript/esm",
resolve: {
mainFields:
options.target === "web" ||
options.target === "webworker" ||
options.target === "electron-renderer"
? ["browser", "main"]
: ["main"]
}
},
{
test: /\.json$/i,
type: "json"
},
{
test: /\.wasm$/i,
type: "webassembly/experimental"
}
]
}

如果想查看更多webpack4相关的默认配置,到这里来。可以看到webpack4把很多插件相关的配置都迁移到了optimization中,但是我们看看官方文档对optimization的介绍简直寥寥无几,而在默认配置的代码中,webpack对optimization的配置有十几项,反正我是怕了。

虽然api发生了一些变化,好的一面就是有了这些默认值,我们想通过webpack构建一个项目比以前要简单很多,如果你只是想简单的进行打包,在package.json中添加如下两个script,包你满意。

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

开发环境使用webpack-dev-server,边预览边打包再也不用f5,简直爽歪歪;生产环境直接生成打包后的文件到dist目录

2、loader与plugin的升级

loader的升级就是一次大换血,之前适配webpack3的loader都需要升级才能适配webpack4。如果你使用了不兼容的loader,webpack会告诉你:

DeprecationWarning: Tapable.apply is deprecated. Call apply on the plugin directly instead

DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks instead

如果在运行过程中遇到这两个警告,就表示你有loader或者plugin没有升级。造成这两个错误的原因是,webpack4使用的新的插件系统,并且破坏性的对api进行了更新,不过好在这只是警告,不会导致程序退出,不过建议最好是进行升级。对于loader最好全部进行一次升级,反正也不亏,百利而无一害。

关于plugin,有两个坑,一个是extract-text-webpack-plugin,还一个是html-webpack-plugin

先说说extract-text-webpack-plugin,这个插件主要用于将多个css合并成一个css,减少http请求,命名时支持contenthash(根据文本内容生成hash)。但是webpack4使用有些问题,所以官方推荐使用mini-css-extract-plugin

⚠️ Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead.

这里改动比较小,只要替换下插件,然后改动下css相关的loader就行了:

-const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = {
module: {
rules: [
{
test: /\.css$/,
- use: ExtractTextPlugin.extract({
- use: [{
- loader: 'css-loader',
- options: {
- minimize: process.env.NODE_ENV === 'production'
- }
- }],
- fallback: 'vue-style-loader'
- })
+ use: [
+ MiniCssExtractPlugin.loader,
+ {
+ loader: 'css-loader',
+ options: {
+ minimize: process.env.NODE_ENV === 'production'
+ }
+ ],
}
]
},
plugins:[
- new ExtractTextPlugin({
+ new MiniCssExtractPlugin({
filename: 'css/[name].css',
}),
...
]
}

然后看看html-webpack-plugin,将这个插件升级到最新版本,一般情况没啥问题,但是有个坑,最好是把chunksSortMode这个选项设置为none。

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins:[
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true,
hash: true,
chunksSortMode: 'none' //如果使用webpack4将该配置项设置为'none'
})
]
}

官方有个issues讨论了这个问题,感兴趣可以去看看。目前作者还在寻找解决方案中。

另外,webpack-dev-server也有个升级版本,叫做webpack-serve,功能比webpack-dev-server强大,支持HTTP2、使用WebSockets做热更新,暂时还在观望中,后续采坑。

3、webpack4的模块拆分

webpack3中,我们经常使用CommonsChunkPlugin进行模块的拆分,将代码中的公共部分,以及变动较少的框架或者库提取到一个单独的文件中,比如我们引入的框架代码(vue、react)。只要页面加载过一次之后,抽离出来的代码就可以放入缓存中,而不是每次加载页面都重新加载全部资源。

CommonsChunkPlugin的常规用法如下:

module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({ //将node_modules中的代码放入vendor.js中
name: "vendor",
minChunks: function(module){
return module.context && module.context.includes("node_modules");
}
}),
new webpack.optimize.CommonsChunkPlugin({ //将webpack中runtime相关的代码放入manifest.js中
name: "manifest",
minChunks: Infinity
}),
]
}

之前CommonsChunkPlugin虽然能用,但是配置不够灵活,难以理解,minChunks有时候为数字,有时候为函数,并且如果同步模块与异步模块都引入了相同的module并不能将公共部分提取出来,最后打包生成的js还是存在相同的module。

现在webpack4使用optimization.splitChunks来进行代码的拆分,使用optimization.runtimeChunk来提取webpack的runtime代码,引入了新的cacheGroups概念。并且webpack4中optimization提供如下默认值,官方称这种默认配置是保持web性能的最佳实践,不要手贱去修改,就算你要改也要多测试(官方就是这么自信)。

module.exports = {
optimization: {
minimize: env === 'production' ? true : false, //是否进行代码压缩
splitChunks: {
chunks: "async",
minSize: 30000, //模块大于30k会被抽离到公共模块
minChunks: 1, //模块出现1次就会被抽离到公共模块
maxAsyncRequests: 5, //异步模块,一次最多只能被加载5个
maxInitialRequests: 3, //入口模块最多只能加载3个
name: true,
cacheGroups: {
default: {
minChunks: 2,
priority: -20
reuseExistingChunk: true,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
}
},
runtimeChunk {
name: "runtime"
}
}
}

有了这些默认配置,我们几乎不需要任何成功就能删除之前CommonChunkPlugin的代码,好神奇。

什么模块会进行提取?

通过判断splitChunks.chunks的值来确定哪些模块会提取公共模块,该配置一共有三个选项,initialasync、 all
默认为async,表示只会提取异步加载模块的公共代码,initial表示只会提取初始入口模块的公共代码,all表示同时提取前两者的代码。

这里有个概念需要明确,webpack中什么是初始入口模块,什么是异步加载模块。e.g.

//webpack.config.js
module.exports = {
entry: {
main: 'src/index.js'
}
} //index.js
import Vue from 'vue'
import(/* webpackChunkName: "asyncModule" */'./a.js')
.then(mod => {
console.log('loaded module a', mod)
}) console.log('initial module')
new Vue({}) //a.js
import _ from 'lodash'
const obj = { name: 'module a' }
export default _.clone(obj)

上面的代码中,index.js在webpack的entry配置中,这是打包的入口,所以这个模块是初始入口模块。再看看index.js中使用了动态import语法,对a.js(该异步模块被命名为asyncModule)进行异步加载,则a.js就是一个异步加载模块。再看看index.jsa.js都有来自node_modules的模块,按照之前的规则,splitChunks.chunks默认为async,所以会被提取到vendors中的只有webpackChunkName中的模块。

如果我们把splitChunks.chunks改成all,main中来自node_modules的模块也会被进行提取了。

module.exports = {
optimization: {
splitChunks: {
chunks: "all"
}
}
}

现在我们在index.js中也引入lodash,看看入口模块和异步模块的公共模块还会不会像CommonsChunkPlugin一样被重复打包。

//index.js
import Vue from 'vue'
import _ from 'lodash' import(/* webpackChunkName: "asyncModule" */'./a.js')
.then(mod => {
console.log('loaded module a', mod)
}) console.log('initial module')
console.log(_.map([1,2,3], a => {
return a * 10
}))
new Vue({}) //a.js
import _ from 'lodash'
const obj = { name: 'module a' }
export default _.clone(obj)

可以看到之前CommonsChunkPlugin的问题已经被解决了,main模块与asyncModule模块共同的lodash都被打包进了vendors~main.js中。

提取的规则是什么?

splitChunks.cacheGroups配置项就是用来表示,会提取到公共模块的一个集合,也就是一个提取规则。像前面的vendor,就是webpack4默认提供的一个cacheGroup,表示来自node_modules的模块为一个集合。

除了cacheGroups配置项外,可以看下其他的几个默认规则。

  1. 被提取的模块必须大于30kb;
  2. 模块被引入的次数必须大于1次;
  3. 对于异步模块,生成的公共模块文件不能超出5个;
  4. 对于入口模块,抽离出的公共模块文件不能超出3个。

对应到代码中就是这四个配置:

{
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
}

三、赠送webpack常见优化方式

1、一个人不行,大家一起上

webpack是一个基于node的前端打包工具,但是node基于v8运行时只能是单线程,但是node中能够fork子进程。所以我们可以使用多进程的方式运行loader,和压缩js,社区有两个插件就是专门干这两个事的:HappyPack、ParallelUglifyPlugin。

使用HappyPack

const path = require('path')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// loader: 'babel-loader'
loader: 'happypack/loader?id=babel'
}
]
},
plugins: [
new require('happypack')({
id: 'babel',
loaders: ['babel-loader']
}),
],
};

使用ParallelUglifyPlugin

module.exports = {
optimization: {
minimizer: [
new require('webpack-parallel-uglify-plugin')({
// 配置项
}),
]
}
}

2、打包再打包

使windows的时候,我们经常会看到一些.dll文件,dll文件被称为动态链接库,里面包含了程序运行时的一些动态函数库,多个程序可以共用一个dll文件,可以减少程序运行时的物理内存。

webpack中我们也可以引入dll的概念,使用DllPlugin插件,将不经常变化的框架代码打包到一个js中,比如叫做dll.js。在打包的过程中,如果检测到某个块已经在dll.js中就不会再打包。之前DllPlugin与CommonsChunkPlugin并能相互兼容,本是同根生相煎何太急。但是升级到webpack4之后,问题就迎刃而解了。

使用DllPlugin的时候,要先写另外一个webpack配置文件,用来生成dll文件。

//webpack.vue.dll.js
const path = require('path') module.exports = {
entry: {
// 把 vue 相关模块的放到一个单独的动态链接库
vue: ['vue', 'vue-router', 'vuex', 'element-ui']
},
output: {
filename: '[name].dll.js', //生成vue.dll.js
path: path.resolve(__dirname, 'dist'),
library: '_dll_[name]'
},
plugins: [
new require('webpack/lib/DllPlugin')({
name: '_dll_[name]',
// manifest.json 描述动态链接库包含了哪些内容
path: path.join(__dirname, 'dist', '[name].manifest.json')
}),
],
};

然后在之前的webpack配置中,引入dll。

const path = require('path')

module.exports = {
plugins: [
// 只要引入manifest.json就能知道哪些模块再dll文件中,在打包过程会忽略这些模块
new require('webpack/lib/DllReferencePlugin')({
manifest: require('./dist/vue.manifest.json'),
})
],
devtool: 'source-map'
};

最后生成html文件的时候,一定要先引入dll文件。

<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<script src="./dist/vue.dll.js"></script>
<script src="./dist/main.js"></script>
</body>
</html>

3、你胖你先跑,部分代码预先运行

前面的优化都是优化打包速度,或者减少重复模块的。这里有一种优化方式,能够减少代码量,并且减少客户端的运行时间。

使用Prepack,这是facebook开源的一款工具,能够运行你的代码中部分能够提前运行的代码,减少在线上真实运行的代码。

官方的demo如下:

//input
(function () {
function hello() { return 'hello'; }
function world() { return 'world'; }
global.s = hello() + ' ' + world();
})(); //output
s = "hello world";

想在webpack中接入也比较简单,社区以及有了对应的插件prepack-webpack-plugin,目前正式环境运用较少,还有些坑,可以继续观望。

module.exports = {
plugins: [
new require('prepack-webpack-plugin')()
]
};

这里简单罗列了一些webpack的优化策略,但是有些优化策略还是还是要酌情考虑。比如多进程跑loader,如果你项目比较小,开了之后可能变慢了,因为本来打包时间就比较短,用来fork子进程的时间,说不定都已经跑完了。记住过早的优化就是万恶之源

四、总结

webpack4带了很多新的特性,也大大加快的打包时间,并且减少了打包后的文件体积。期待webpack5的更多新特性,比如,以html或css为文件入口(鄙人认为html才是前端模块化的真正入口,浏览器的入口就是html,浏览器在真正的亲爹,不和爹亲和谁亲),默认开启多进程打包,加入文件的长期缓存,更多的拓展零配置。

[转] 初探webpack4的更多相关文章

  1. webpack4配置详解之新手上路初探

    前言 经常会有群友问起webpack.react.redux.甚至create-react-app配置等等方面的问题,有些是我也不懂的,慢慢从大家的相互交流中,也学到了不少. ​ 今天就尝试着一起来聊 ...

  2. Webpack4 学习笔记一初探Webpack

    前言 此内容是个人学习笔记,以便日后翻阅.非教程,如有错误还请指出 Webpack 打包文件 支持JS模块化 模式: production(0配置默认), development(生产环境) 更详细的 ...

  3. 初探Parcel

    昨天趁有点时间看了前不久很火的构建工具Parcel,这里说下初步使用的感受,尤其是将其放到实际项目中和Webpack进行比较. 一.前言 首先说下笔者目前的技术栈.最近的前端项目主要以管理后台为主,技 ...

  4. 初探领域驱动设计(2)Repository在DDD中的应用

    概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...

  5. CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探

    CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码 ...

  6. 从273二手车的M站点初探js模块化编程

    前言 这几天在看273M站点时被他们的页面交互方式所吸引,他们的首页是采用三次加载+分页的方式.也就说分为大分页和小分页两种交互.大分页就是通过分页按钮来操作,小分页是通过下拉(向下滑动)时异步加载数 ...

  7. JavaScript学习(一) —— 环境搭建与JavaScript初探

    1.开发环境搭建 本系列教程的开发工具,我们采用HBuilder. 可以去网上下载最新的版本,然后解压一下就能直接用了.学习JavaScript,环境搭建是非常简单的,或者说,只要你有一个浏览器,一个 ...

  8. .NET文件并发与RabbitMQ(初探RabbitMQ)

    本文版权归博客园和作者吴双本人共同所有.欢迎转载,转载和爬虫请注明原文地址:http://www.cnblogs.com/tdws/p/5860668.html 想必MQ这两个字母对于各位前辈们和老司 ...

  9. React Native初探

    前言 很久之前就想研究React Native了,但是一直没有落地的机会,我一直认为一个技术要有落地的场景才有研究的意义,刚好最近迎来了新的APP,在可控的范围内,我们可以在上面做任何想做的事情. P ...

随机推荐

  1. <Android基础> (六) 数据存储 Part 3 SQLite数据库存储

    6.4 SQLite数据库存储 SQLite是一种轻量级的关系型数据库,运算速度快,占用资源少. 6.4.1 创建数据库 Android为了管理数据库,专门提供了SQLiteOpenHelper帮助类 ...

  2. 【一本通1248:Dungeon Master&&洛谷UVA532 Dungeon Master】

    若不会广搜转向[广搜] [题目描述] 这题是一个三维的迷宫题目,其中用‘.’表示空地,‘#’表示障碍物,‘S’表示起点,‘E’表示终点,求从起点到终点的最小移动次数,解法和二维的类似,只是在行动时除了 ...

  3. pytest 13 使用自定义标记mark

    前言: pytest可以规定那些要跑,那些不跑,跑特定的哪些?比如以下的这个例子: #!/usr/bin/env/python # -*-coding:utf-8-*- import pytest @ ...

  4. 20175221 《Java程序设计》迭代和JDB(课下作业,选做):

    20175221 <Java程序设计> 迭代和JDB(课下作业,选做): 任务详情 1 使用C(n,m)=C(n-1,m-1)+C(n-1,m)公式进行递归编程实现求组合数C(m,n)的功 ...

  5. 初识Kotlin之变量

    用Java开发了很多年,因为工作的需要学习Kotlin.初识Kotlin时是各种不习惯,觉得这个语言相对于Java而言并不够严谨.随着不断的深入,最终还是逃不过"真香定理".我一直 ...

  6. 2018-2019-2 20165232 《网络对抗技术》 Exp6 信息搜集与漏洞扫描

    2018-2019-2 20165232 <网络对抗技术> Exp6 信息搜集与漏洞扫描 一.实践目标 掌握信息搜集的最基础技能与常用工具的使用方法. 二.实践内容. 各种搜索技巧的应 D ...

  7. Spring Cloud中关于Feign的常见问题总结

    一.FeignClient接口,不能使用@GettingMapping 之类的组合注解 代码示例: @FeignClient("microservice-provider-user" ...

  8. 五十六、linux 编程——UDP 编程模型

    56.1 UDP 编程模型 56.1.1 编程模型 UDP 协议称为用户数据报文协议,可靠性比 TCP 低,但执行效率高 56.1.2 API (1)发送数据 函数参数: sockfs:套接字文件描述 ...

  9. 第31月 第9天 责任链AppDelegate

    1.AppDelegate 到这里我们把解决方案再明确一下:把 AppDelegate 的调用解耦成一个 责任链 模型.并且满足以下特征: 每个模块都可以无限制的实现 AppDelegate 的方法. ...

  10. APK改之理 手游修改改编安卓程序工具安装使用教程

    APK改之理 手游修改改编安卓程序工具安装使用教程 --APK破解付费程序 apk改之理是pc平台上一款非常好用的apk反编译工具,他将反编译以及签名等功能集中在一起,并且拥有非常人性化的操作界面,如 ...