管理资源&管理输出
管理资源
webpack
最出色的功能之一就是,除了JavaScript
,还可以通过loader
引入任何其他类型的文件。webpack.config.js
配置如下:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
},
{
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},
{
test: /\.xml$/,
use: [
'xml-loader'
]
}
]
}
};
管理输出
配置
HtmlWebpackPlugin
插件自动生成index.html
;配置CleanWebpackPlugin
插件自动清理dist
目录;WebpackManifestPlugin
插件可以提取manifest
。webpack.config.js
配置如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
开发环境&生产环境
- 开发环境(
development
)和生产环境(production
)的构建目标差异很大。 - 在开发环境中,我们需要具有强大的、具有实时重新加载(
live reloading
)或热模块替换(hot module replacement
)能力的source map
和localhost server
。 - 而在生产环境中,我们的目标则转向于关注更小的
bundle
,更轻量的source map
,以及更优化的资源,以改善加载时间。 - 由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的
webpack
配置。 - 但是,我们会遵循不重复原则,保留一个“通用”配置。我们将使用一个名为
webpack-merge
的工具合并配置
npm install --save-dev webpack-merge
开发模式&模块热替换
生产环境
- 代码压缩:
UglifyJSPlugin
或其他工具 -
source map
用还是不用? - 许多
library
将通过与process.env.NODE_ENV
环境变量关联,以决定library
中应该引用哪些内容
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
-
ExtractTextPlugin
:将CSS
分离成单独的文件 -
CLI
替代选项:原来用的一些插件现在可以替换成一些优化配置项:例如,--optimize-minimize
标记将在后台引用UglifyJSPlugin
。和以上描述的DefinePlugin
实例相同,--define process.env.NODE_ENV="'production'"
也会做同样的事情。并且,webpack -p
将自动地调用上述这些标记,从而调用需要引入的插件。
构建性能
- 保持版本最新:
webpack/node/npm
- 尽量少使用不同的工具
loader/plugins
;必要的话也用在尽量最少数的必要模块中;可以将非常消耗资源的 loaders 转存到worker pool
中
//使用 include 字段仅将 loader 模块应用在实际需要用其转换的位置中:
{
test: /\.js$/,
include: path.resolve(__dirname, "src"),
loader: "babel-loader"
}
-
Dlls
:使用DllPlugin
将更改不频繁的代码进行单独编译。这将改善引用程序的编译速度,即使它增加了构建过程的复杂性。 -
Smaller = Faster
:减少编译的整体大小,以提高构建性能。尽量保持chunks
小巧。 - 使用
cache-loader
启用持久化缓存。使用package.json
中的postinstall
清除缓存目录。thread-loader
可以将非常消耗资源的loaders
转存到worker pool
中 - 提高解析速度:
- 尽量减少
resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles
中类目的数量,因为他们会增加文件系统调用的次数。 - 如果你不使用
symlinks
,可以设置resolve.symlinks: false
。 - 如果你使用自定义解析
plugins
,并且没有指定context
信息,可以设置resolve.cacheWithContext: false
。
- 尽量减少
- 开发环境中
- 硬避免在生产环境下才会用到的工具:
UglifyJsPlugin、ExtractTextPlugin、[hash]/[chunkhash]、AggressiveSplittingPlugin、AggressiveMergingPlugin、ModuleConcatenationPlugin
- 不同的
devtool
的设置,会导致不同的性能差异 - 在内存中进行代码的编译和资源的提供,但并不写入磁盘来提高性能:
webpack-dev-server
- 尽量减少入口
chunk
的体积,以提高性能
- 硬避免在生产环境下才会用到的工具:
- 其他
tree shaking
tree shaking
是一个术语,通常用于描述移除JavaScript
上下文中的未引用代码(dead-code
)。新的webpack 4
正式版本,扩展了这个检测能力,通过package.json
的sideEffects
属性作为标记,向compiler
提供提示,表明项目中的哪些文件是pure
(纯的ES2015
模块)",由此可以安全地删除文件中未使用的部分。
-
sideEffects
:将文件标记为无副作用
如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。
{
"name": "your-project",
"sideEffects": false
}
如果你的代码确实有一些副作用,那么可以改为提供一个数组:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js"
]
}
- 压缩输出:通过如上方式,我们可以,找出那些需要删除的“未使用代码”,然而,我们不只是要找出,还需要在
bundle
中删除它们。为此,我们将使用-p(production)
这个webpack
编译标记,来启用uglifyjs
压缩插件。- 注意,
--optimize-minimize
标记也会在webpack
内部调用UglifyJsPlugin
。) - 从
webpack 4
开始,也可以通过mode
配置选项轻松切换到压缩输出,只需设置为production
。
- 注意,
懒加载(动态导入/按需加载)
在
vue
单页应用中,当项目不断完善丰富时,即使使用webpack
打包,文件依然是非常大的,影响页面的加载。如果我们能把不同路由对应的组件分割成不同的代码块,当路由被访问时才加载对应的组件(也就是按需加载),这样就更加高效了。——引自vue-router
官方文档
非动态加载时的打包情况如下图:
如下案例中
hello
组件的加载方式改为路由懒加载[import()语法],在进行打包
// import Hello from '@/components/Hello'
export default new Router({
routes: [
{
path: '/',
name: 'Hello',
// component: Hello,
component: () => import('@/components/Hello')
}
]
})
- 很明显的看到,打包后有
4个js
文件,仔细的同学还发现,app.js
文件的大小加上新多出文件的大小,约等于没有分割打包的app
的大小。 - 这样等于异步加载的组件,是单独打包成了一个
js
,在页面首次加载的时候不需要加载他,等到请求相应的页面的时候在去服务器请求它,减小了页面首屏加载的时间。 -
webpack.prod.conf.js
中配置output.chunkFilename
规定了打包异步文件的格式
//webpack.prod.conf.js:生产打包js
//utils.assetsPath是utils.js中封装的一个路径相关的方法
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
/**
本人在自己的工程中删除或修改chunkFilename的配置,
最后还是都按这个规则生产js文件了,
Why?
*/
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
-
不需要刻意懒加载。下面的案例中,代码确实会在脚本运行的时候产生一个分离的代码块
lodash.bundle.js
,在技术概念上“懒加载”它。问题是加载这个包并不需要用户的交互 -- 意思是每次加载页面的时候都会请求它。这样做并没有对我们有很多帮助,还会对性能产生负面影响。
//src/index.js
- import _ from 'lodash';
-
- function component() {
+ function getComponent() {
- var element = document.createElement('div');
-
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
+ var element = document.createElement('div');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+ return element;
+
+ }).catch(error => 'An error occurred while loading the component');
}
- document.body.appendChild(component());
+ getComponent().then(component => {
+ document.body.appendChild(component);
+ })
- 当页面有个按钮点击事件时,需要加载某个组件,这种用户交互的情景下,我们可以使用懒加载;当然,路由也是交互的一种。
缓存
客户端(通常是浏览器)获取资源是比较耗费时间的。可以通过命中缓存,以降低网络流量,使网站加载速度更快;然而,缓存的存在可能会使你获取新代码时比较棘手(文件名不变的话)。如何通过必要的配置,以确保
webpack
编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。
- 通过使用
output.filename
进行文件名替换
- 如果我们不做修改,然后再次运行构建,我们以为文件名会保持不变。然而,如果我们真的运行,可能会发现情况并非如此(译注:如果不做修改,文件名可能会变,也可能不会。)
- 这也是因为
webpack
在入口chunk
中,包含了某些样板(boilerplate
),特别是runtime
和manifest
。(译注:样板(boilerplate
)指webpack
运行时的引导代码) - 即:如果
webpack
生成的hash
发生改变,manifest
文件也会发生改变。因此,vendor bundle
的内容也会发生改变,并且失效。所以,我们需要将manifest
文件提取出来。 - 用
CommonsChunkPlugin
将manifest
提取出来
-
webpack
里每个模块都有一个module id
,module id
是该模块在模块依赖关系图里按顺序分配的序号,如果这个module id
发生了变化,那么他的chunkhash
也会发生变化。 - 这样会导致:如果你引入一个新的模块,会导致
module id
整体发生改变,可能会导致所有文件的chunkhash
发生变化,这显然不是我们想要的 - 这里需要用
HashedModuleIdsPlugin
,根据模块的相对路径生成一个四位数的hash作为模块id,这样就算引入了新的模块,也不会影响module id
的值,只要模块的路径不改变的话。
new webpack.HashedModuleIdsPlugin()
创建library
除了打包应用程序代码,
webpack
还可以用于打包JavaScript library
;即我们平时npm install
下来的那种依赖包
最后,
package.json
中配置"main": "dist/ginna-form.js"
;我们从node_modules
引入时就靠这个属性来找到对应的文件的哦。
shimming
webpack
编译器(compiler
)能够识别遵循ES2015
模块语法、CommonJS
或AMD
规范编写的模块。然而,一些第三方的库(library
)可能会引用一些全局依赖(例如jQuery
中的$
)。这些库也可能创建一些需要被导出的全局变量。 这些“不符合规范的模块”就是shimming
发挥作用的地方
shim
:一种库(library
)的抽象,这种库能将一个新的API
引入到一个旧的环境中,而且仅靠旧的环境中已有的手段实现。polyfill
就是一个用在浏览器API
上的shim
。
1. 让jQuery
作为全局变量,可以被别的组件引用
2. 将模块中的this
置为window
当模块运行在 CommonJS
环境下this
会变成一个问题,也就是说此时的 this
指向的是 module.exports
。在这个例子中,你可以通过使用 imports-loader
覆写 this
:
3. 某个库(library)创建出一个全局变量,它期望用户使用这个变量
- 你可能从来没有在自己的源码中做过这些事情,但是你也许遇到过一个老旧的库(
library
),和下面所展示的代码类似。 - 在下图用例中,我们可以使用
exports-loader
,将一个全局变量作为一个普通的模块来导出。例如,为了将file
导出为file
以及将helpers.parse
导出为parse
4. babel-polyfill
按需加载
-
Babel
是一个广泛使用的转码器,可以将ES6
代码转为ES5
代码,从而可以在现有环境执行,所以我们可以用ES6
编写,而不用考虑环境支持的问题。 -
Babel
默认只转换新的JavaScript
语法(syntax
),如箭头函数等,而不转换新的API
,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise
等全局对象,以及一些定义在全局对象上的方法(比如Object.assign
)都不会转码;因此我们需要polyfill
;
5. 深度优化包babel-preset-env
-
babel-preset-env
是一个新的preset
,可以根据配置的目标运行环境(environment
)自动启用需要的babel
插件 - 之前我们写
javascript
代码时,需要使用N
个preset
,比如:babel-preset-es2015、babel-preset-es2016
。es2015
可以把ES6
代码编译为ES5
,es2016
可以把ES2016
代码编译为ES6
。babel-preset-latest
可以编译stage 4
进度的ECMAScript
代码。 - 问题是我们几乎每个项目中都使用了非常多的
preset
,包括不必要的。 -
babel-preset-env
的工作方式类似babel-preset-latest
,唯一不同的就是它会根据配置的env
只编译那些还不支持的特性。 - 使用这个插件,你讲再也不需要使用
es20xx presets
了。
6. 其他工具
- 还有一些其他的工具能够帮助我们处理这些老旧的模块。
-
script-loader
会在全局上下文中对代码进行取值,类似于通过一个script
标签引入脚本。在这种模式下,每一个标准的库(library
)都应该能正常运行 - 这些老旧的模块如果没有
AMD/CommonJS
规范版本,但你也想将他们加入dist
文件,你可以使用noParse
来标识出这个模块
noParse:这是module中的一个属性,
作用:不去解析属性值代表的库的依赖
举例:
我们一般引用jquery,可以如下引用:
import jq from 'jquery'
对于上面的解析规则:
当解析jq的时候,会去解析jq这个库是否有依赖其他的包
我们对类似jq这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。
所以,对于这类不引用其他的包的库,我们在打包的时候就没有必要去解析,
这样能够增加打包速率。
所以,可以在webpack的配置中增加noParse属性
(以下代码只需要看module的noParse属性)
module.exports = {
mode:'development',
entry:'./src/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
module:{
noParse:/jquery/,//不去解析jquery中的依赖库
rules:[ ]
}
}
渐进式网络应用程序PWA
渐进式网络应用程序(
Progressive Web Application - PWA
),是一种可以提供类似于原生应用程序(native app
)体验的网络应用程序(web app
)。PWA
可以用来做很多事。其中最重要的是,在离线(offline
)时应用程序能够继续运行功能。这是通过使用名为Service Workers
的网络技术来实现的。
//print.js
export default function printMe() {
console.log('I get called from print.js!');
}
//index.js
import printMe from './print.js';
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
function component() {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
var btn = document.createElement('button');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
//3个插件都需要npm install --save-dev
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
title: 'Progressive Web Application'
}),
//添加 Workbox
new WorkboxPlugin.GenerateSW({
// 这些选项帮助 ServiceWorkers 快速启用
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
//package.json配置脚本
"scripts": {
"build": "webpack",
//使用一个简易服务器,搭建出我们所需的离线体验
//npm install http-server --save-dev
"start": "http-server dist"
},
- 有了
Workbox
,我们再看下执行npm run build
时会发生什么
clean-webpack-plugin: /mnt/c/Source/webpack-follow-along/dist has been removed.
Hash: 6588e31715d9be04be25
Version: webpack 3.10.0
Time: 782ms
Asset Size Chunks Chunk Names
app.bundle.js 545 kB 0, 1 [emitted] [big] app
print.bundle.js 2.74 kB 1 [emitted] print
index.html 254 bytes [emitted]
precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js 268 bytes [emitted]
service-worker.js 1 kB [emitted]
[0] ./src/print.js 87 bytes {0} {1} [built]
[1] ./src/index.js 477 bytes {0} [built]
[3] (webpack)/buildin/global.js 509 bytes {0} [built]
[4] (webpack)/buildin/module.js 517 bytes {0} [built]
+ 1 hidden module
Child html-webpack-plugin for "index.html":
1 asset
[2] (webpack)/buildin/global.js 509 bytes {0} [built]
[3] (webpack)/buildin/module.js 517 bytes {0} [built]
+ 2 hidden modules
- 现在你可以看到,生成了 2 个额外的文件:
service-worker.js
(或sw.js
) 和体积很大的precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
。sw.js
是Service Worker
文件,precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
是sw.js
引用的文件,所以它也可以运行 - 然后用
npm start
启动服务。访问http://localhost:8080/index.html
并查看console
控制台。在那里你应该看到:
SW registered
- 现在来进行测试。停止服务器并刷新页面。如果浏览器能够支持
Service Worker
,你应该可以看到你的应用程序还在正常运行。然而,服务器已经停止了服务,此刻是Service Worker
在提供服务。
TypeScript
准备工作:
ts-loader
插件、tsconfig.json
配置文件(和package.json
同级)、ts
文件中如何使用第三方库(ts
声明文件*.d.ts
)
-
webpack.config.js
配置
-
tsconfig.json
案例:
- 使用第三方库
当从npm
安装第三方库时,一定要牢记同时安装这个库的类型声明文件。你可以从TypeSearch
中找到并安装这些第三方库的类型声明文件。举个例子,如果想安装lodash
这个库的类型声明文件,我们可以运行下面的命令:npm install --save-dev @types/lodash
TypeScript
学习