一、为什么使用webpack,webpack的优点是什么?
- 专注于处理模块化项目,能做到 开箱即用、一步到位
- 可通过Plugin扩展,完整好用又不失灵活;
- 使用场景不局限与web开发;
- 社区庞大活跃,经常引入紧跟时代发展的新特性,能够为大数场景找到开源扩展;
- 良好的开发体验;
二、webpack核心概念
- Entry: 入口,webpack执行构建的第一步将从Entry开始,可抽象成输入。
- Module: 模块,在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。
- Chunk:代码块,一个Chunk有多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于将模块的原内容按照需求转换成新内容
- Plugin:扩展插件,在Webpack构建流程中的特定时机注入扩展逻辑,来改变构建结果或做成我们想要的事情。
三、流程概况
webpack的运行流程是一个串行的过程,从启动到结束会依次执行一下流程。
- 初始化参数: 从配置文件和Shell语句中读取与合并参数,得出最终的参数。
- 开始编译: 用上一步得到参数初始化comilper对象,加载所有配置的插件,通过执行对象的run方法开始执行编译。
- 确定入口:根据配置中的entry找出所有入口文件。
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行编译,在找出该模块依赖的模块,在递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容及他们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再将每个chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,在根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。
四、基本配置
1)entry 入口
// entry 表示 入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
// 类型可以是 string | object | array
entry: './app/entry', // 只有1个入口,入口只有1个文件
entry: ['./app/entry1', './app/entry2'], // 只有1个入口,入口有2个文件
entry: { // 有2个入口
a: './app/entry-a',
b: ['./app/entry-b1', './app/entry-b2']
},
// 项目实战
entry: {client: resolve('../src/client.js'),},
?输出一个什么样的js文件
entry: resolve('../src/client.js'),
?输出一个什么样的js文件
2)output
filename & chunkFilename
变量名 | 含义 |
---|---|
id | Chunk的唯一标识,从0开始 |
name | Chunk的名称 |
hash | Chunk的唯一标识的Hash值 |
chunkhash | Chunk内容的Hash值 |
output: {
path: resolve('../dist'),
filename: dev? '[name].bundle.js' : '[name].[chunkhash:4].js',
chunkFilename: dev ? 'chunks/[name].js' : 'chunks/[name].[chunkhash:4].js',
},
publicPath
// 如何输出结果:在 Webpack 经过一系列处理后,如何输出最终想要的代码。
output: {
// 输出文件存放的目录,必须是 string 类型的绝对路径。
path: path.resolve(__dirname, 'dist'),
// 发布到线上的所有资源的 URL 前缀,string 类型
publicPath: '/assets/', // 放到指定目录下
publicPath: '', // 放到根目录下
publicPath: 'https://cdn.example.com/', // 放到 CDN 上去
},
3)Module
Loader配置
rules配置模块的读取和解析规则,通常用来配置Loader。其类型是一个数组,数组里的每一项都描述了如何处理部分文件。
- 条件匹配:通过test、include、exclude三个配置来选中Loader要应用规则的文件
- 应用规则:对选中的文件通过user配置项来应用Loader,可以只应用一个Loader或者按照从后往前的顺序应用一组Loader,同时可以分别向Loader传入参数。
- 重置顺序:一组Loader的执行顺序默认是从右向左执行,通过enforce选项可以将其中一个Loader的执行顺序放到最前或者最后
module: { // 配置模块相关
rules: [ // 配置 Loader
{
test: /\.jsx?$/, // 正则匹配命中要使用 Loader 的文件
include: [ // 只会命中这里面的文件
path.resolve(__dirname, 'app')
],
exclude: [ // 忽略这里面的文件
path.resolve(__dirname, 'app/demo-files')
],
use: ['style-loader', 'css-loader','sass-loader' ]
// 使用那些 Loader,有先后次序,从后往前执行
enforce:'post',
// enforce:'post'的含义是将该Loader的执行顺序放到最后
// enforce:'pre',代表将Loader的执行顺序放到最前面
}, ], }
noParse
noParse配置项可以让Webpack忽略对部分没有采用模块化的文件的递归解析和处理,这样做能提高构建性能。
// 使用正则表达式
noParse: /jquery|chartjs/,
// 使用函数,从webpack3.0.0开始
noParse: ()=>{
// content 代表一个模块的文件路径
// 返回true或fasle
retrun /jquery|chartjs/test(content);
}
4)Resolve
// 配置寻找模块的规则
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中,_dirname表示当前工作目录,也就是项目根目录
modules: [ // 寻找模块的根目录,array 类型,默认以 node_modules 为根目录
'node_modules',
path.resolve(__dirname, 'app')
],
extensions: ['.js', '.json', '.jsx', '.css'], // 模块的后缀名
alias: { // 模块别名配置,用于映射模块
// 把 'module' 映射 'new-module',同样的 'module/path/file' 也会被映射成 'new-module/path/file'
'module': 'new-module',
// 使用结尾符号 $ 后,把 'only-module' 映射成 'new-module',
// 但是不像上面的,'module/path/file' 不会被映射成 'new-module/path/file'
'only-module$': 'new-module',
},
],
mainFields: ['jsnext:main','browser','main'],
// 第三方模块会针对不同的环境提供几分代码。例如分别提供采用了ES5和ES6的两份代码,
例如
{
"jsnext:main":"es/index.js", //采用ES6语法的代码入口
"main":"lib/index.js", //采用ES6语法的代码入口
}
enforceExtension: false, // 是否强制导入语句必须要写明文件后缀
},
5)DevServer
devServer: { // DevServer 相关的配置
proxy: { // 代理到后端服务接口
'/api': 'http://localhost:3000'
},
contentBase: path.join(__dirname, 'public'),
// 配置 DevServer HTTP 服务器的文件根目录
compress: true, // 是否开启 gzip 压缩
historyApiFallback: true, // 是否开发 HTML5 History API 网页
hot: true, // 是否开启模块热替换功能
https: false, // 是否开启 HTTPS 模式
},
6)Watch和WatchOptions
watch: true, // 是否开始
watchOptions: { // 监听模式选项
// 不监听的文件或文件夹,支持正则匹配。默认为空
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
// 默认为300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是不停的去询问系统指定文件有没有变化,默认每隔1000毫秒询问一次
poll: 1000
五、常用插件介绍
1)HtmlWebpackPlugin
inject属性参数
参数 | 含义 |
---|---|
ture | 默认值,scirpt标签位于html文件的body底部 |
body | script标签位于html文件的body底部 |
head | script标签位于html文件的head中 |
false | 不插入生成的js文件,这个几乎用不到 |
chunksSortModet属性参数
参数 | 含义 |
---|---|
manual | 按照手动的顺序载入 |
dependency | 按照不同文件的依赖关系来排序 |
auto | 默认值,插件的内置的排序方式,具体顺序.... |
none | 无序 |
const HtmlWebpackPlugin = require('html-webpack-plugin')
new HtmlWebpackPlugin({
template: resolve('../src/index.html'),
inject: true,
// chunks: htmlWebpackConfig.chunks,
// chunksSortMode: 'manual'
}),
// webpack3 举例
// 需要chunks的包列表,支持正则
let chunksPackage = {
'antd': ['antd'],
'antdesign': ['^_@ant-design_icons@'],
'react': ['react', 'react-router', 'react-redux', 'redux'],
'immutable': ['immutable', 'moment', 'core-js','lodash','axios'],
'bcsd': ['^_rc-'],
}
exports.chunksWebpackConfig = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
children: true,
async: 'async-common',
minChunks: 3,
}),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
...Object.keys(chunksPackage).map(packageName => {
return new webpack.optimize.CommonsChunkPlugin({
name: packageName,
chunks: ['vendor'],
minChunks: function (module, count) {
return module.resource && chunksPackage[packageName].filter(item => new RegExp(item).test(getModuleName(module)))[0] && count >= 0
}
})
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: Infinity,
}),
],
}
exports.htmlWebpackConfig = {
chunks: ['manifest', 'react', 'bcsd', 'immutable', 'antdesign', 'antd', 'vendor', 'client'],
}
参考文章:[https://segmentfault.com/a/1190000013883242](https://segmentfault.com/a/1190000013883242)
2)BundleAnalyzerPlugin
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// 分析代码
new BundleAnalyzerPlugin({
analyzerPort: 3011
}),
new BundleAnalyzerPlugin({
analyzerMode: 'static'
}),
详细配置
new BundleAnalyzerPlugin({
// 可以是`server`,`static`或`disabled`。
// 在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
// 在“ static”模式下,会生成带有报告的单个HTML文件。
// 在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
analyzerMode: 'server',
// 将在“服务器”模式下使用的主机启动HTTP服务器。
analyzerHost: '127.0.0.1',
// 将在“服务器”模式下使用的端口启动HTTP服务器。
analyzerPort: 8888,
// 路径捆绑,将在`static`模式下生成的报告文件。
// 相对于捆绑输出目录。
reportFilename: 'report.html',
// 模块大小默认显示在报告中。
// 应该是`stat`,`parsed`或者`gzip`中的一个。
// 有关更多信息,请参见“定义”一节。
defaultSizes: 'parsed',
// 在默认浏览器中自动打开报告
openAnalyzer: true,
// 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
generateStatsFile: false,
// 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
// 相对于捆绑输出目录。
statsFilename: 'stats.json',
// stats.toJson()方法的选项。
// 例如,您可以使用`source:false`选项排除统计文件中模块的来源。
// 在这里查看更多选项:https: //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
statsOptions: null,
logLevel: 'info' //日志级别。可以是'信息','警告','错误'或'沉默'。
}),
六、优化
1、拆包
- webpack3
1)CommonsChunkPlugin
具体参考HtmlWebpackPlugin插件
- webpack4
2)splitChunks
function getModulePackageName(module) {
// 获取模块依赖名
const context = module.context;
if (!context) return null;
const nodeModulesPath = path.join(__dirname, '../node_modules/');
if (context.substring(0, nodeModulesPath.length) !== nodeModulesPath) {
return null;
}
const moduleRelativePath = context.substring(nodeModulesPath.length);
const [moduleDirName] = moduleRelativePath.split(path.sep);
let packageName = moduleDirName;
if (packageName.match('^_')) {
packageName = packageName.match(/^_(@?[^@]+)/)[1];
}
return packageName;
}
optimization: {
concatenateModules: true, // 使用scope hoisting
usedExports: true, // 开启tree shaking 只打包使用到的模块
noEmitOnErrors: true, // 跳过编译异常阶段
removeAvailableModules: true, // 当这些模块已包含在所有父项中时,检测并从块中删除模块
removeEmptyChunks: true, // 检测并删除空的块
mergeDuplicateChunks: true, // 合并包含相同模块的块
// runtimeChunk: false,
namedChunks: true, // 入口文件发生改变依然产生稳定hash
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
chunks: 'async',
// 必须三选一: "initial" | "all" | "async" (默认就是async)
minSize: 30000,// 最小30k以上会进行拆分
maxSize: 0,
minChunks: 1,
automaticNameDelimiter: '~',// 命名分隔符
name: true, // 强制恢复启用文件名称
// maxAsyncRequests: 5,
maxInitialRequests: Infinity,
cacheGroups: {// 这里开始设置缓存的 chunks
vendors: {
chunks: 'initial',
priority: 40,
name: 'vendors',
test: module => {
const packageName = getModulePackageName(module);
return packageName && ['core-js', 'draft-js', 'moment'].includes(packageName);
},
},
antdicons: {
chunks: 'all',
priority: 30,
name: 'antdicons',
reuseExistingChunk: true,
test: module => {
const packageName = getModulePackageName(module);
return packageName && (packageName.includes('@ant-design_icons') || packageName.includes('@ant-design'));
},
},
antdlib: {
// antd/lib
chunks: 'all',
priority: 20,
reuseExistingChunk: true,
// minSize: 400000,
// maxSize: 700000,
test: module => {
const packageName = getModulePackageName(module);
return packageName && ['antd'].includes(packageName);
},
name(module) {
const packageName = getModulePackageName(module);
if (packageName && ['antd'].includes(packageName) && ++index < 140) {
// 前140个文件打包到lib1中
return 'antdlib1'
}
return 'antdlib2';
},
},
rclibs: {
chunks: 'all',
priority: 10,
reuseExistingChunk: true,
test: module => {
const packageName = getModulePackageName(module);
return packageName && packageName.includes('rc-')
},
name(module) {
const packageName = getModulePackageName(module);
if (['rc-calendar', 'rc-tree-select', 'rc-select', 'rc-table',
'rc-menu', 'rc-tree'].includes(packageName)) {
return 'rc_libs'
}
return 'rc_others';
},
},
others: {
chunks: 'initial',
test: /[\\/]node_modules[\\/]/,
priority: 0,
name: 'others',
// // minSize: 400000,
// // maxSize: 700000,
// test: module => {
// const packageName = getModulePackageName(module);
// return packageName && !packageName.includes('@ant-design');
// },
reuseExistingChunk: true,
},
// default: {
// minChunks: 2,
// priority: -20,
// name: 'default',
// reuseExistingChunk: true
// }
}
},
},
},
// 推荐文章 [https://juejin.im/post/5af1677c6fb9a07ab508dabb](https://juejin.im/post/5af1677c6fb9a07ab508dabb)
2、DLLPlugin
要给web项目构建接入动态链接库的思想
- 将网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中。在一个动态链接库中可以包含多个模块。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次打包,而是去动态链接库中获取。
- 页面依赖的所有动态链接库都需要被加载
为什么会提高打包速度:
包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中的代码。
实例一
var path = require("path");
var webpack = require("webpack");
module.exports = {
// 要打包的模块的数组
mode: 'production',
entry: {
react: ['react', 'react-dom', 'react-router'],
redux: ['redux', 'react-redux', 'redux-actions', 'redux-thunk', 'react-router-redux'],
vendors: ['dragula', 'axios', 'habo', 'immutable']
},
output: {
path: path.join(__dirname, '../dll'), // 打包后文件输出的位置
filename: '[name].dll.js',// vendor.dll.js中暴露出的全局变量名。
library: '[name]', // 打包出的是一个库,暴漏到全局,名叫vendors
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json'),
// context: __dirname
}),
]
};
function getPlugins() {
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
if (/.*.dll.js/.test(file)) {
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
// 将打包后的dll文件注入html中
}));
} else if (/.*.manifest.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
}));
// 分析json文件,里面有的模块就不会引用node_modules里的文件,因为dll.js中已存在
}
});
return plugins;
}
plugins: getPlugins()
实例二
const path = require("path");
const webpack = require("webpack");
const { dependencies } = require("./package.json");
module.exports = {
mode: "development",
entry: {
vendor: Object.keys(dependencies)
},
output: {
path: path.join(__dirname, "dll"), // 生成的dll.js路径,我是存在/build/dev中
filename: "[name].dll.js", // 生成的文件名字
library: "[name]_library" // 生成文件的一些映射关系,与下面DllPlugin中配置对应
},
plugins: [
// 使用DllPlugin插件编译上面配置的NPM包
new webpack.DllPlugin({
// 会生成一个json文件,里面是关于dll.js的一些配置信息
path: path.join(__dirname, "dll", "[name]-manifest.json"),
name: "[name]_library", // 与上面output中配置对应
context: __dirname
})
]
};
3、HappyPack
实例一
// 使用happypack进行多进程打包
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
new HappyPack({
//用id来标识 happypack处理那里类文件
id: 'happyBabel',
//如何处理 用法和loader 的配置一样
loaders: [{
loader: 'babel-loader?cacheDirectory',
}],
//共享进程池
threadPool: happyThreadPool,
//允许 HappyPack 输出日志
verbose: true,
}), // happypack 实例
new HappyPack({
id: 'happyStyl',
loaders: [{
loader: 'style-loader!css-loader!stylus-loader',
}],
threadPool: happyThreadPool,
verbose: true,
})
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'happypack/loader?id=happyBabel',
// 使用happypack loader
exclude: /node_modules/
},
{
test: /\.styl$/,
// loader: 'style-loader!css-loader!stylus-loader',
loader: 'happypack/loader?id=happyStyl',
// 使用另一个happypack处理
//exclude: /node_modules/
include: path.resolve(__dirname, '../src')
},
}
4、压缩
var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
// 自定义js优化配置,将会覆盖默认配置
new UglifyJsPlugin({
exclude: /\.min\.js$/, // 过滤掉以".min.js"结尾的文件,我们认为这个后缀本身就是已经压缩好的代码,没必要进行二次压缩
cache: true,
parallel: true, // 开启并行压缩,充分利用cpu
sourceMap: false,
extractComments: false, // 移除注释
uglifyOptions: {
compress: {
unused: true,
warnings: false,// 在uglifyjs删除没有用到代码时不输出警告
drop_debugger: true, // 删除debugger
},
output: {
beautify: false, // 最紧凑的输出
comments: false,// 删除所有注释
}
}
}),
]
}
}
5、Tree Shaking 和 Scope Hoisting
optimization: {
concatenateModules: true, // 使用scope hoisting
usedExports: true, // 开启tree shaking 只打包使用到的模块
noEmitOnErrors: true, // 跳过编译异常阶段
removeAvailableModules: true,
// 当这些模块已包含在所有父项中时,检测并从块中删除模块
removeEmptyChunks: true, // 检测并删除空的块
mergeDuplicateChunks: true, // 合并包含相同模块的块
// runtimeChunk: false,
namedChunks: true, // 入口文件发生改变依然产生稳定hash
runtimeChunk: {
name: 'manifest'
},
splitChunks: {}
},
},