Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过loader
的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS
等。
Webpack 官网
Webpack 中文指南
将你的配置信息写到多个分散的文件中去,然后在执行webpack的时候利用--config参数指定要加载的配置文件,配置文件利用module
imports
导出。你可以在webpack/react-starter看到是使用这种发方法的。
//配置文件
|-- webpack-dev-server.config.js
|-- webpack-hot-dev-server.config.js
|-- webpack-production.config.js
|-- webpack.config.js
//npm 命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-server": "webpack-dev-server --config webpack-dev-server.config.js --progress --colors --port 2992 --inline",
"hot-dev-server": "webpack-dev-server --config webpack-hot-dev-server.config.js --hot --progress --colors --port 2992 --inline",
"build": "webpack --config webpack-production.config.js --progress --profile --colors"
},
调用第三方的webpack工具,使用其集成的api,方便进行webpack配置。HenrikJoreteg/hjs-webpack 这个repo
就是这么做的。
var getConfig = require('hjs-webpack')module.exports = getConfig({
// entry point for the app
in: 'src/app.js',
// Name or full path of output directory
// commonly named www
or public
. This
// is where your fully static site should
// end up for simple deployment.
out: 'public',
// This will destroy and re-create your
// out
folder before building so you always
// get a fresh folder. Usually you want this
// but since it's destructive we make it
// false by default
clearBeforeBuild: true
})
ones that can be reused and combined with other partial configurations
在单个配置文件中维护配置,但是区分好条件分支。调用不同的npm
命令时候设置不同的环境变量,然后在分支中匹配,返回我们需要的配置文件。
这样做的好处可以在一个文件中管理不同npm
操作的逻辑,并且可以共用相同的配置。webpack-merge这个模块可以起到合并配置的作用。
const parts = require('./webpack-config/parts');
switch(process.env.npm_lifecycle_event) {
case 'build':config = merge(common,
parts<span>.clean</span>(PATHS<span>.build</span>),
parts<span>.setupSourceMapForBuild</span>(),
parts<span>.setupCSS</span>(PATHS<span>.app</span>),
parts<span>.extractBundle</span>({
name: 'vendor',
entries: ['react', 'vue', 'vuex']
}),
parts<span>.setFreeVariable</span>('process<span>.env</span><span>.NODE_ENV</span>', 'production'),
parts<span>.minify</span>()
);
<span>break;
default:
config = merge(common,
parts<span>.setupSourceMapForDev</span>(),
parts<span>.devServer</span>(),
parts<span>.setupCSS</span>(PATHS<span>.app</span>));
}
// minify example
exports.minify = function () {
return {
plugins: [ <span>new</span> <span>webpack.optimize.UglifyJsPlugin({</span>
compress: {
warnings: false,
drop_console: true
<span>},</span>
comments: false,
beautify: false
<span>})</span>
<span>]</span>
}
}
webpack-dev-server
在webpack的watch基础上开启服务器。webpack-dev-server
是运行在内存中的开发服务器,支持高级webpack特性hot module replacement
。这对于react vue
这种组件化开发是很方便的。
使用webpack-dev-server
命令开启服务器,配合HMR
及可以实现代码更改浏览器局部刷新的能力。
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running without a page reload.
当应用在运行期间hmr机制能够修改、添加、或者移除相应的模块,而不使整个页面刷新。
hmr
机制适用于单页应用。
要实现hmr
机制,需要配合webpack-dev-server
服务器,这个服务器本身就实现了监察watch
文件改动的能力,再开启HMR
选项,就添加了watch
模块变化的能力。这是HMR
机制能生效的基础。
每次修改一个模块的时候,webpack会生成两部分,一个是manifest.json
,另一部分是关于这次模块更新编译完成的chunks
。manifest.json
中存着的是chunk
更改前后的hash值。
从编译器webpack的角度来讲提供了hmr
的原材料。供后续使用。
模块发生变化时,webpack会生成之前讲过的两部分基础文件,但是何时将变化后的模块应用到app中去?这里就需要在应用代码中编写handler去接受到模块变化信息。但是不能在所有模块中编写handler吧?这里就用到了消息冒泡机制。
如图A.js、C.js没有相关hmr代码,B.js有相关hmr代码,如果c模块发生了变化,c模块没有hmr,那么就会冒泡到a、b模块。b模块捕捉到了消息,hmr运行时会相应的执行一些操作,而a.js捕捉不到信息,会冒泡到entry.js,而一旦有消息冒泡的入口块,这就代表本次hmr失败了,hmr会降级进行整个页面的reload。
HMR运行时是一些相关的操作api,运行时支持两个方法: check、apply
。
check发起 HTTP 请求去获取更新的 manifest
,以及一些更新过后的chunk。
var env = {'process.env.NODE_ENV': '"production"'
}
new webpack.DefinePlugin(env)
注意这里单引号间多了个双引号 why?
以及webpack.DefinePlugin
插件的原理?
开发的时候会想写很多只在开发环境出现的代码,比如接口mock
等,在build
命令后这些代码不会存在。
这对框架或者插件、组件的开发是很有帮助的。vue,react
等都会这么做。可以在这些框架的dev
模式提供很多有用的提示信息。
对于一个单页应用项目来说,有分为业务代码和第三方代码,业务代码会频繁改动,而第三方代码一般来讲变动的次数较少,如果每次修改业务代码都需要用户将整个js文件都重新下载一遍,对于加载性能来讲是不可取的,所以一般而言我们会将代码分为业务代码和第三方代码分别进行打包,虽然多了一个请求的文件,增加了一些网络开销,但是相比于浏览器能将文件进行缓存而言,这些开销是微不足道的。
我们在entry中定义了app入口,相应的业务逻辑都封装在这个入口文件里,如果我们想要第三方代码独立出来,就要再增加一个入口,我们习惯使用vendor
这个命名。
// app.js
require('vue');
require('vuex');// webpack.config.js
entry: {
app: 'app/app.js',
vendor: ['vue', 'vuex'],
},
vendor
入口的传参是以一个数组的形式传递的,这是一种非常方便的注入多个依赖的方式,并且能把多个依赖一起打包到一个chunk中。而且不用手动的创建真实存在的入口文件。
这相当于:
// vendor.js
require('vue');
require('vuex');// app.js
require('vue');
require('vuex');
// webpack.config.js
entry: {
app: 'app/app.js',
vendor: 'app/vendor.js',
},
但是这样做只是声明了一个vendor
入口而已,对于app这个入口来说,打包完成的文件还是会有vue
和vuex
依赖,而新增的入口vendor
打包完成的文件也有了vue
和vuex
两个依赖。模块依赖关系如下图所示。
这里的A可以代表vue
依赖,最后生成的打包文件是两个平行关系的文件,且都包含vue
的依赖。
此时需要引入CommonsChunkPlugin
插件
This is a pretty complex plugin. It fundamentally allows us to extract all the common modules from different bundles and add them to the common bundle. If a common bundle does not exist, then it creates a new one.
这是个相当复杂的插件,他的基础功能是允许我们从不同的打包文件中抽离出相同的模块,然后将这些模块加到公共打包文件中。如果公共打包文件不存在,则新增一个。同时这个插件也会将运行时(runtime)转移到公共chunk打包文件中。
plugins: [
new webpack.optimize.CommonsChunkPlugin({names: ['vendor', 'manifest']
})
]
这里的name可以选择已经存在的块,这里就选择了vendor
块,因为我们本来就是将vendor
块当做管理第三方代码的入口的。
而names传入一个数组,数组里包含两个trunk name,表示CommonsChunkPlugin
插件会执行两次这个方法,第一次将公共的第三方代码抽离移到vendor
的块中,这个过程之前也讲过会将运行时runtime也转移到vendor
块中,第二次执行则是将运行时runtime抽离出来转移到manifest
块中。这步操作解决了缓存问题。
这样处理,最后会生成3个打包文件chunk
,app.js
是业务代码,vendor
则是公共的第三方代码,manifest.js
则是运行时。
webpack1.0官网介绍中的chunk类型读起来及其拗口chunk type, 所以我这里解读一下。
chunk是webpack中最基本的概念之一,且chunk
常常会和entry
弄混淆。在「打包文件分割部分」我们定义了两个入口entry point -- app
和vendor
,而通过一些配置,webpack会生成最后的一些打包文件,在这个例子中最后生成的文件有``app.js 、 vendor.js 、 manifest.js。这些文件便被称为块chunk。
在官方1.0文档中webpack的chunk类型分为三种:
entry chunk 入口块不能由字面意思理解为由入口文件编译得到的文件,由官网介绍
An entry chunk contains the runtime plus a bunch of modules
可以理解为包含runtime运行时的块可以称为entry chunk
,一旦原本存在运行时(runtime)的entry chunk
失去了运行时,这个块就会转而变成initial chunk
。
A normal chunk contains no runtime. It only contains a bunch of modules.
普通块不包含运行时runtime,只包含一系列模块。但是在应用运行时,普通块可以动态的进行加载。通常会以jsonp
的包装方式进行加载。而code splitting
主要使用的就是普通块。
An initial chunk is a normal chunk.
官方对initial chunk
的定义非常简单,初始块就是普通块,跟普通块相同的是同样不包含运行时runtime,不同的是初始块是计算在初始加载过程时间内的。在介绍入口块entry chunk
的时候也介绍过,一旦入口块失去了运行时,就会变成初始块。这个转变经常由CommonsChunkPlugin
插件实现。
还是拿「打包文件分割」的代码做例子,
// app.js
require('vue');
require('vuex');// webpack.config.js
entry: {
app: 'app/app.js',
vendor: ['vue', 'vuex'],
},
没有使用CommonsChunkPlugin
插件之前,两个entry
分别被打包成两个chunk
,而这两个chunk
每个都包含了运行时,此时被称为entry chunk
入口块。
而一旦使用了CommonsChunkPlugin
插件,运行时runtime最终被转移到了manifest.js
文件,此时最终打包生成的三个chunkapp.js 、 vendor.js 、 manifest.js,app.js、vendor.js
失去了runtime就由入口块变成初始块。
前文有讲到将依赖分割开来有助于浏览器缓存,提高用户加载速度,但是当业务复杂度增加,代码量大始终是一个问题。这时候就需要normal chunk
普通块的动态加载能力了。
It allows you to split your code into various bundles which you can then load on demand — like when a user navigates to a matching route, or on an event from the user.
code splitting 允许我们将代码分割到可以按需加载的不同的打包文件中,当用户导航到对应的路由上时,或者是用户触发一个事件时,异步加载相应的代码。
我们需要在业务逻辑中手动添加一些分割点,标明此处事件逻辑之后进行代码块的异步加载。
// test
window.addEventListener('click', function () {
require.ensure(['vue', 'vuex'], function (require) {})
})
这段代码表明当用户点击时,异步请求一个js文件,这个文件中包含该有vue vuex
的依赖。
打包后会根据手动分割点的信息生成一个打包文件,就是图中第一行0开头的文件。这个文件也就是异步加载的文件。
下面是之前的一个vue
项目,采用code splitting
将几个路由抽离出来异步加载之后,文件由212kb减少到了137kb,同样样式文件也由58kb减少到了7kb。对于首屏渲染来说,性能是会增加不少的。