Webpack几个概念:javascript
chunk: chunk是webpack拆分出来的:css
问题分析:html
解决思路:前端
如何拆分,方式因人而异,因项目而异。拆分原则有:vue
经过将公共模块拆出来,最终合成的文件在最开始的时候加载一次,便存到缓存中供后续使用。这个带来速度上的提高,由于浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。java
webpack提供了一个很是好的内置插件帮咱们实现这一需求:CommonsChunkPlugin
。不过在 webpack4 中CommonsChunkPlugin
被删除,取而代之的是optimization.splitChunks
。
CommonsChunkPlugin
CommonsChunkPlugin
插件,是一个可选的用于创建一个独立文件(又称做 chunk)的功能,这个文件包括多个入口 chunk
的公共模块。node
childrenreact
minChunks含义:
数字:模块被多少个chunk公共引用才被抽取出来成为commons chunk
函数:接受 (module, count) 两个参数,返回一个布尔值,你能够在函数内进行你规定好的逻辑来决定某个模块是否提取成为commons chunk
Infinity: 只有当入口文件(entry chunks) >= 3 才生效,用来在第三方库中分离自定义的公共模块
1. 分离出第三方库、自定义公共模块、webpack运行文件, 放在同一个文件中webpack
修改webpack.config.js新增一个入口文件vendor, 使用CommonsChunkPlugin插件进行公共模块的提取:git
const path = require("path"); const webpack = require("webpack"); const packageJson = require("./package.json"); module.exports = { entry: { first: './src/first.js', second: './src/second.js', // 新增一个入口文件vendor vendor: Object.keys(packageJson.dependencies) }, output: { path: path.resolve(__dirname,'./dist'), filename: '[name].js' }, plugins: [ // 使用CommonsChunkPlugin插件进行公共模块的提取 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[name].js' }), ] }
生成dist文件夹下文件有:first.js, second.js, vendor.js。
经过查看vendor.js文件,发现first.js和second.js文件中依赖的第三方库和自定义公共模块都被打包进vendor.js中,同时还有webpack的运行文件。
2. 单独分离出第三方库、自定义公共模块、webpack运行文件
plugins: [ // 抽离第三方库与webpack运行文件 new webpack.optimize.CommonsChunkPlugin({ name: ['vendor','runtime'], // 建立runtime.js进行webpack运行文件的抽离,其中source chunks是vendor.js filename: '[name].js', minChunks: Infinity }), // 抽离自定义公共模块 new webpack.optimize.CommonsChunkPlugin({ name: 'common', filename: '[name].js', chunks: ['first','second'],// 从first.js和second.js中抽取commons chunk }), ]
生成dist文件夹下文件有:first.js, second.js, vendor.js, runtime.js, common.js
splitChunks
cacheGroups
是splitChunks
配置的核心,在cacheGroups
缓存组里配置代码的拆分规则。缓存组的每个属性都是一个配置规则, 例如配置default属性,属性名能够不叫default能够本身定。属性的值是一个对象。index/a.js
这样的。all
,async
,initial
。all
表明全部模块,async
表明异步加载的模块, initial
表明初始化时就能获取的模块。若是是函数,则能够根据chunk参数的name等属性进行更细致的筛选。minChunks:splitChunks
是自带默认配置的,而缓存组默认会继承这些配置,其中有个minChunks
属性:
minSize设置生成文件的最小大小,单位是字节。若是一个模块符合以前所说的拆分规则,可是若是提取出来最后生成文件大小比minSize要小,那它不会被提取出来。这个属性能够在每一个缓存组属性中设置,也能够在splitChunks属性中设置,在每一个缓存组都会继承这个配置。
priority设置拆分规则的优先级,属性值为数字,能够为负数。当某个模块同时符合一个以上的规则时,经过优先级属性priority来决定使用哪一个拆分规则。优先级高者执行。
test设置缓存组选择的模块,与chunks属性的做用有一点像,可是维度不同。test的值能够是一个正则表达式,也能够是一个函数。它能够匹配模块的绝对资源路径或chunk名称,匹配chunk名称时,将选择chunk中的全部模块。
1. 实现代码分离:
//webpack.config.js optimization: { splitChunks: { cacheGroups: { default: { name: 'common', chunks: 'initial', minChunks: 2, //模块被引用2次以上的才抽离 } } } }
进入dist目录查看:common.js
: 包含引用2次以上的全部模块
2. 分离第三方库与自定义组件库
//webpack.config.js optimization: { splitChunks: { minSize: 300, //提取出的chunk的最小大小 cacheGroups: { default: { name: 'common', chunks: 'initial', minChunks: 2, //模块被引用2次以上的才抽离 priority: -20, }, // 拆分第三方库(经过npm|yarn安装的库) vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'initial', priority: -10, }, // 拆分指定文件 locallib: { test: /(src\/locallib\.js)$/, name: 'locallib', chunks: 'initial', priority: -9 } } } }
进入dist目录查看:common.js
,vendor.js
包含第三方库代码,locallib.js
包含locallib
模块的代码。
DLL(Dynamic Link Library)文件为动态连接库文件。在Windows中,许多应用程序并非一个完整的可执行文件,它们被分割成一些相对独立的动态连接库,即DLL文件,放置于系统中。当咱们执行某一个程序时,相应的DLL文件就会被调用。
一般来讲,咱们的代码均可以致少简单区分红业务代码和第三方库。若是不作处理,每次构建时都须要把全部的代码从新构建一次,耗费大量的时间。而后大部分状况下,不少第三方库的代码并不会发生变动(除非是版本升级),这时就能够用到dll:把复用性较高的第三方模块打包到动态连接库中,在不升级这些库的状况下,动态库不须要从新打包,每次构建只从新打包业务代码。
DllPlugin
和 DllReferencePlugin
用某种方法实现了拆分 bundles,大幅度提高了构建的速度。使用DLL时,能够把构建过程分红dll构建过程和主构建过程,因此须要两个构建配置文件,例如叫作webpack.config.js
和webpack.dll.config.js
。
1. 使用DLLPlugin
打包须要分离到动态库的模块
DllPlugin
是webpack
内置的插件,不须要额外安装,直接配置webpack.dll.config.js
文件。此插件用于在单独的 webpack 配置中建立一个 dll-only-bundle,会生成一个名为 manifest.json
的文件,这个文件是用于让 DllReferencePlugin
可以映射到相应的依赖上。
context
(可选): manifest 文件中请求的 context (默认值为 webpack 的 context)format
(boolean = false):若是为 true
,则 manifest json 文件 (输出文件) 将被格式化。name
:暴露出的 DLL 的函数名(TemplatePaths:[fullhash]
& [name]
)path
:manifest.json 文件的 绝对路径(输出文件)entryOnly
(boolean = true):若是为 true
,则仅暴露入口type
:dll bundle 的类型
咱们建议 DllPlugin 只在
entryOnly: true
时使用,不然 DLL 中的 tree shaking 将没法工做,由于全部 exports 都可使用。
// webpack.dll.config.js module.exports = { entry: { // 第三方库 react: ['react', 'react-dom', 'react-redux'] }, output: { // 输出的动态连接库的文件名称,[name] 表明当前动态连接库的名称, filename: '[name].dll.js', path: resolve('dist/dll'), // library必须和后面dllplugin中的name一致 library: '[name]_dll_[hash]' }, plugins: [ new webpack.DllPlugin({ // 动态连接库的全局变量名称,须要和 output.library 中保持一致 // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值 name: '[name]_dll_[hash]', // 描述动态连接库的 manifest.json 文件输出时的文件名称 path: path.join(__dirname, 'dist/dll', '[name].manifest.json') }), ] }
2. 在主构建配置文件使用DllReferencePlugin引用动态库文件
在webpack.config.js
中使用dll要用到DllReferencePlugin
, 此插件会把 dll-only-bundles 引用到须要的预编译的依赖中。
context
:(绝对路径) manifest (或者是内容属性)中请求的上下文extensions
:用于解析 dll bundle 中模块的扩展名 (仅在使用 'scope' 时使用)。manifest
:包含 content
和 name
的对象,或者是一个字符串 —— 编译时用于加载 JSON manifest 的绝对路径content
(可选): 请求到模块 id 的映射(默认值为 manifest.content
)name
(可选):dll 暴露地方的名称(默认值为 manifest.name
)(可参考externals
)scope
(可选):dll 中内容的前缀sourceType
(可选):dll 是如何暴露的 (libraryTarget)经过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,以后再在须要的时候经过内置的 __webpack_require__
函数来 require
对应的模块。
new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./dist/dll/react.manifest.json') }),
第一步产出的manifest
文件就用在这里,给主构建流程做为查找dll的依据:DllReferencePlugin去 manifest.json 文件读取 name 字段的值,把值的内容做为在从全局变量中获取动态连接库中内容时的全局变量名,所以:在 webpack.dll.config.js 文件中,DllPlugin 中的 name 参数必须和 output.library 中保持一致。
3. 在入口文件引入dll文件
生成的dll暴露出的是全局函数,所以还须要在入口文件里面引入对应的dll文件。
<body> <div id="app"></div> <!--引用dll文件--> <script src="../../dist/dll/react.dll.js"></script> </body>
1.分离代码,业务代码和第三方模块能够被打包到不一样的文件里,这个有几个好处:
2.提高构建速度。第三方库没有变动时,因为咱们只构建业务相关代码,相比所有从新构建天然要快的多。
moment.js
日期处理库,占用很大的体积, 由于全部的locale
文件都被引入,而这些文件在整个库的体积中占了大部分,所以当webpack打包时移除这部份内容会让打包文件的体积有所减少。
webpack自带的两个库能够实现这个功能:
IgnorePlugin
的使用方法以下:
// 插件配置 plugins: [ // 忽略moment.js中全部的locale文件 new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], // 使用方式 const moment = require('moment'); // 引入zh-cn locale文件 require('moment/locale/zh-cn'); moment.locale('zh-cn');
ContextReplacementPlugin
的使用方法以下:
// 插件配置 plugins: [ // 只加载locale zh-cn文件 new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/), ], // 使用方式 const moment = require('moment'); moment.locale('zh-cn');
在项目中使用了lodash
这个很经常使用的工具库,然而在使用这类工具库的时候每每只使用到了其中的不多的一部分功能,但却把整个库都引入了。所以这里也能够进一步优化,只引用须要的部分。
import { chain, cloneDeep } from 'lodash'; // 或者 import chain from 'lodash/chain'; import cloneDeep from 'lodash/cloneDeep';
咱们日常也会对代码进行压缩混淆,能够经过UglifyJS
等工具来对js代码进行压缩,同时能够去掉没必要要的空格、注释、console信息等,也能够有效的减少代码体积。
hash通常是结合CDN缓存来使用,经过webpack构建以后,生成对应文件名自动带上对应的MD5值。若是文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。
hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改。同一次构建过程当中生成的哈希都是同样的。
output:{ path:path.join(__dirname, '/dist'), filename: 'bundle.[name].[hash].js', }
根据不一样的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。把一些公共库和程序入口文件区分开,单独打包构建,接着咱们采用chunkhash的方式生成哈希值,那么只要咱们不改动公共库的代码,就能够保证其哈希值不会受影响。
output:{ path:path.join(__dirname, '/dist/js'), filename: 'bundle.[name].[chunkhash].js', }
采用chunkhash,项目主入口文件Index.js及其对应的依赖文件Index.css因为被打包在同一个模块,共用相同的chunkhash。因为公共库是不一样的模块,有单独的chunkhash。因此Index文件的更改不会影响公共库。若是index.js更改了代码,css未改变,因为该模块发生了改变,致使css文件会重复构建。
根据文件内容建立出惟一 hash。当文件内容发生变化时,[contenthash] 才会发生变化。
output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', path: path.resolve(__dirname, '../dist'), }
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它容许在运行时更新全部类型的模块,而无需彻底刷新。
实现
// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); { devServer: { contentBase: './dist', hot: true, // DevServer开启模块热替换模式 }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Hot Module Replacement', }), ], }
filename
指列在 entry
中,打包后输出的文件的名称。
chunkFilename
指未列在 entry
中,却又须要被打包出来的文件的名称。默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id] 或 [id].)。
// webpack.config.js module.exports = { entry: './src/index.js', output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), chunkFileName: '[name].bundle.js' } }
由于 css 不是编程语言,因此不能声明变量、函数,不能作判断、循环和计算,也不能嵌套。为了解决这个问题,衍生了两种拓展语言 less 与 sass,它们兼容 css,而且拓展了编程的功能,主要是带来了如下的特性:
@import
避免重复导入问题,所以能够放心大胆的导入其余文件。从模块化的角度来说,less 与 sass 只是扩充了 css 的功能,但并无在语言的层面作模块化,由于全局命名冲突的问题依然还在。
想要让 css 具有模块化功能,暂时还不能从语言的层面来考虑,因此只能从工具的角度来实现。目前比较好的方式是使用 js
来加载 css
文件,并将 css
的内容导出为一个对象,使用 js
来渲染整个 dom 树和匹配相应的样式到对应的元素。
css文件建议遵循以下原则
.class
才能导出为对象的属性)composes
组合来实现复用.className
书写,而非 .class-name
(前者能够经过 styles.className
访问,后者须要经过 styles['class-name']
才能访问)。实例
/* dialog.css */ .root {} .confirm {} .disabledConfirm {}
js文件引入dialog.css
, 使用 classnames 库来操做 class 名:
/* dialog.jsx */ import classNames from 'classnames'; import styles from './dialog.css'; export default class Dialog extends React.Component { render() { const cx = classNames({ [styles.confirm]: !this.state.disabled, [styles.disabledConfirm]: this.state.disabled }); return <div className={styles.root}> <a className={cx}>Confirm</a> ... </div> } }
若是你不想频繁的输入styles.**
,能够试一下 react-css-modules,它经过高阶函数的形式来避免重复输入styles.**
。
依赖webpack: css-loader
这个功能须要构建工具的支持,若是使用 webpack ,可使用 css-loader,并设置 options.modules
为 true
, 即可使用模块化的功能了。css-loader
解析@import和 url() ,会 import/require()
后再解析(resolve)它们。css-loader
配置项:
名称 | 类型 | 默认值 | 描述 | |
---|---|---|---|---|
root | String | root值将被添加到 URL 前面,而后再进行转译。由于对于以 / 开头的 URL,默认行为是不转译。 |
||
url | Boolean | true | 启用/禁用解析 url() |
|
alias | Object | {} | 给url建立别名。用别名重写你的 URL,在难以改变输入文件的url 路径时,这会颇有帮助。 | |
import | Boolean | true | 启用/禁用 @import 处理 | |
minimize | Boolean\ | Object | false | 启用/禁用 压缩 |
sourceMap | Boolean | false | 启用/禁用 Sourcemap | |
importLoaders | Number | 0 | 在 css-loader 前应用的 loader 的数量 | |
modules | Boolean | false | 启用/禁用 CSS 模块 | |
camelCase | Boolean\ | String | false | 是否以驼峰化式命名导出类名 |
localIdentName | String | [hash:base64] | 配置生成的类名标识符(ident) |
module.exports = { module: { rules: [ { test: /\.css$/i, loader: "css-loader", options: { modules: true, }, }, ], }, };
loader:
是一个转换器,将A文件进行编译成B文件,好比:将A.less转换为A.css,单纯的文件转换过程。
是一个导出为function的node模块。能够将匹配到的文件进行一次转换,同时loader能够链式传递。
plugin:
是一个扩展器,经过钩子能够涉及整个构建流程,能够作一些在构建范围内的事情。
它并不直接操做文件,而是基于事件机制工做,会监听webpack打包过程当中的某些节点,执行普遍的任务。
sass-loader
转化sass为css文件,而且包一层module.exports成为一个js module。
css-loader
解析@import和 url() 。
style-loader
将建立一个style标签将css文件嵌入到html中。
vue-loader、coffee-loader、babel-loader
等能够将特定文件格式转成js模块、将其余语言转化为js语言和编译下一代js语言。
file-loader
能够处理资源,file-loader能够复制和放置资源位置,并能够指定文件名模板,用hash命名更好利用缓存。
url-loader
能够处理资源, 将小于配置limit大小的文件转换成内敛Data Url的方式,减小请求。
raw-loader
能够将文件以字符串的形式返回
imports-loader、exports-loader
能够向模块注入变量或者提供导出模块功能。
UglifyJsPlugin
,压缩和混淆代码。CommonsChunkPlugin
,将指定的模块或公用模块打包出来,减小主bundle文件的体积,配合缓存策略,加快应用访问速度。DllPlugin
和DllReferencePlugin
相互配合,前置第三方包的构建,只构建业务代码,同时能解决Externals屡次引用问题。DllReferencePlugin引用DllPlugin配置生成的manifest.json文件,manifest.json包含了依赖模块和module id的映射关系html-webpack-plugin
能够根据模板自动生成html代码,并自动引用css和js文件extract-text-webpack-plugin
将js文件中引用的样式单独抽离成css文件HotModuleReplacementPlugin
热更新optimize-css-assets-webpack-plugin
不一样组件中重复的css能够快速去重webpack-bundle-analyzer
一个webpack的bundle文件分析工具,将bundle文件以可交互缩放的treemap的形式展现。compression-webpack-plugin
生产环境可采用gzip压缩JS和CSShappypack
:经过多进程模型,来加速代码构建clean-wenpack-plugin
清理每次打包后没有使用的文件为何要分离第三方库?
第三方库是比较稳定,不会轻易改变的,利用浏览器缓存后,用户再次加载页面会减小服务器请求,提升速度优化体验。提取多个应用(入口)公共模块的做用和他相似,公共部分会被缓存,全部应用均可以利用缓存内容从而提升性能。
分离第三方库就能利用浏览器换缓存了么?
答案是否认的。致使没法利用缓存的因素有不少,好比每次分离的库文件从新打包都会获得不一样的名称,后台的同事给js文件设置的缓存过时时间为0,只要文件是彻底不变的,包括修改时间,文件内容等,依然会利用缓存。
浏览器缓存机制是什么样的?
HTTP1.1给的策略是使用Cache-control配合Etag。
Apache中,ETag的值默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后获得的。若是Etag相同,依然不会请求新资源,而会使用之前的文件。
CommonsChunkPlugin与SplitChunksPlugin
做用
将公共模块抽离。每次打包的时候都会从新打包,仍是会去处理一些第三方依赖库,只是它能把第三方库文件和咱们的代码分开掉,生成一个独立的 js 文件。可是它仍是不能提升打包的速度。
自 webpack 4.0 上线以后,CommonsChunkPlugin 已被替换成 SplitChunksPlugin,旨在优化 chunk 的拆分。
CommonsChunkPlugin
设计思路:知足 minChunks 的引用次数时,都会将对应的模块抽离如一个新的 chunk 文件中,这个文件为全部的业务文件的父级。
这种设计思路带来了会形成模块打包冗余。总的来讲会形成这么几个问题:
SplitChunksPlugin
SplitChunksPlugin 优化了 webpack 的打包策略,使用自动重复算法,会自动计算出各页面公共的包引用以及部分页面公共的包引用,固然,对于那些部分共有可是阈值太小的文件其不会建立单独的输出文件,由于其大小不值得去新开一个请求。(缓存策略配置在 cacheGroup 中)
SplitChunksPlugin 默认的分包策略基于如下 4 个条件:
- SplitChunksPlugin 配合使用 RuntimeChunk 对运行时的 hash 变更作优化(至关于 CommonsChunkPlugin 的两次使用)
- 减小
maxInitial/AsyncRequest
会加大 module 的冗余,可是会进一步的减小请求。
DllPlugin与DllReferencePlugin
使用
DLLPlugin 这个插件是在一个额外独立的 webpack 设置中建立一个只有 dll 的 bundle,也就是说,除了 webpack.config.js,项目中还会新建一个 webpack.dll.config.js 文件来配置 dll 的打包。webpack.dll.config.js 做用是把全部的第三方库依赖打包到一个 bundle 的 dll 文件里面,还会生成一个名为 manifest.json 文件。该 manifest.json 的做用是用来让 DllReferencePlugin 映射到相关的依赖上去的。(可类比 CommonsChunkPlugin 的两次打包或者 RuntimeChunk 的运行包配置)
设计思路
DLLPlugin 是提早将公共的包构建出来,使得在 build 时过滤掉这些构建过的包,使得在正是构建时的速度缩短。因此其相对来讲打包速度会更快。
webpack的运行过程能够简单概述为以下流程:
初始化配置参数 -> 绑定事件钩子回调 -> 肯定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件
什么是webpack事件流?
Webpack 就像一条生产线,要通过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每一个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源作处理。Webpack 经过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程当中会广播事件,插件只须要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运做。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -- 吴浩麟《深刻浅出webpack》
咱们将webpack事件流理解为webpack构建过程当中的一系列事件,他们分别表示着不一样的构建周期和状态,咱们能够像在浏览器上监听click事件同样监听事件流上的事件,而且为它们挂载事件回调。咱们也能够自定义事件并在合适时机进行广播,这一切都是使用了webpack自带的模块 Tapable
进行管理的。咱们不须要自行安装 Tapable
,在webpack被安装的同时它也会一并被安装,如需使用,咱们只须要在文件里直接 require
便可。
Tapable的原理
Tapable的原理其实就是咱们在前端进阶过程当中都会经历的EventEmit,经过发布者-订阅者模式实现,它的部分核心代码能够归纳成下面这样:
class SyncHook{ constructor(){ this.hooks = []; } // 订阅事件 tap(name, fn){ this.hooks.push(fn); } // 发布 call(){ this.hooks.forEach(hook => hook(...arguments)); } }
webpack.config.js
文件,初始化本次构建的配置参数,而且执行配置文件中的插件实例化语句,生成Compiler传入plugin的apply方法,为webpack事件流挂上自定义钩子。require
语法替换成__webpack_require__
来模拟模块化操做。compilation.assets
上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。