从基础到实战 手把手带你掌握新版webpack4.0

634 阅读9分钟

现有引入方式的区分:

  • 1、es moudle的模块引入方式
  • import header from './header.js'
  • 2、common js的模块引入方式
  • const header = require('./header.js')
  • 3、CMD的模块引入方式
  • @import url("fineprint.css") print
  • 4、AMD的模块引入方式
  • define([name: String], [dependencies: String[]], factoryMethod: function(...));
  • 5、样式中的引入
  • url(...)或者<img src="..">

webpack 是什么

  • 1、webpack是1个模块打包工具,module bundler.
  • 2、npx 可以在当前文件夹中使用。
  • 3、webpack-cli使得我们在命令行中可以使用webpack命令。(cli:command-line interface命令行界面)
  • 4、chunks指的是每个文件对应的id,chunk names指的是每个文件对应的名字。

loader 是什么

  • webpack默认只编译js文件
  • loader的执行顺序是从下到上,从右到左
  • 1、url-loader和file-loader都是编译jpg|png|gif等图片文件,url-loader比style-loader多个功能,可以在options设置limit,小于limit,就转成base64,大于就直接导出
  • 2、css-loader和style-loader可以编译css文件,css-loader会找到所有的css文件,编译css文件,style-loader转成style挂载在head标签中的style标签中
  • 3、sass-loader编译scss文件

plugins是什么

  • plugin 可以在webpack运行到某个时刻的时候,帮你打包一些事情。
  • htmlWebpackPlugin 会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中。
  • cleanWebpackPlugin 会在打包之前先执行。htmlWebpackPlugin是在打包之后再运行。

sourceMap是什么

  • sourceMap是一个映射关系,他知道打包生成的dist目录中的某一行对应源代码中的哪一些,通过sourceMap可以知道对应源代码中的某一行。从而更方便调试。
  • inline-source-map 当使用时,生成的.map文件会变成base64文件集合到js里面。
  • cheap-inline-source-map 知道是哪一行出错而不用知道是哪一列,减少性能消耗。
  • cheap-module-inline-source-map 加了module主要作用是不仅管业务代码中的报错,还负责第三方模块中的报错。
  • eval 不是通过base64映射对应关系,而是通过eval去映射,执行效率最快,性能最好。但是在比较复杂的代码情况下,提示出来的内容可能不全面。
  • 最佳实践配置:在mode为‘development’模式下,cheap-module-eval-source-map,配置最全,打包速度最快; 在mode为‘production’模式下,使用cheap-module-source-map配置更好。
  • sourceMap的原理:
  • segmentfault.com/a/119000000… www.html5rocks.com/en/tutorial… www.ruanyifeng.com/blog/2013/0… www.youtube.com/watch?v=NkV…
  • sourceMap使用VLQ编码。

webpackDevServer提升开发效率

  • 改变代码,线上打包就会自动生效的几种方式
    • 1、webpack --watch 不会起一个服务器,而且不会重新起浏览器,比较麻烦,所以不太建议使用。
    • 2、使用webpack-dev-server 启动1个http服务器,还会帮助我们打包代码,不会输出到dist目录,而是在内存中。
    • 3、使用webpackDevMiddleware结合express()进行处理。
  • 在node中直接使用webpack
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const config = require('./webpack.config.js')
// 在node中直接使用webpack
const compiler = webpack(config)
const app = express()

app.use(webpackDevMiddleware(compiler, {}))
app.listen(3000, () => {
    console.log('server is running.')
})

Hot Module Replacement 热模块替换,hmr:热更新

  • 可以在我们写css的时候,帮忙调试css

babel-loader

  • 可以将es6的代码转换成es5的代码
  • 如果写的是业务代码,那么需要引入@babel/polyfill,并且配置@babel/preset-env
  • 如果写的是第三方库代码,那么需要引入@babel/plugin-transform-runtime,并且配置plugins
  • 示例代码:
options: {
    // 这里要引入@babel-polyfill
    presets: [['@babel/preset-env', {
        targets: {
            chrome: '67'
        },
        useBuiltIns: 'usage'
        }]]
    // 这里不用引入@babel-polyfill
    plugins: [['@babel/plugin-transform-runtime', {
        'corejs': 2,
        'helpers': true,
        'regenerator': true,
        'useESModules': false
    }]]
}

配置react代码的打包

  • 使用@babel/preset-react 如:
{
    presets: [['@babel/preset-react']]
}

devServer

devServer: { // 可以启动一个服务器运行
    contentBase: path.resolve(__dirname, 'dist'), //在哪个目录中为服务器提供内容。
    compress: true, // 使用gzip进行压缩 
    port: 9000, // 端口号 
    hot: true, // 启动webpack模块的热替换功能
    hotOnly: true // 去掉hotOnly配置可以不用在浏览器上手动刷新页面,修改内容后会自动刷新。
}

Tree Shaking

  • tree-shaking 指的是当引入一个模块的时候,不引入模块的所有代码,只引入某一块代码。
  • Tree Shaking 只支持es module,不支持common js方式,require的引入

development和production的区别

  • sourceMap在development的配置比较全,可以帮助我们在开发环境下快速定位代码的报错位置。在production环境下比较简洁一些,或者可以生成一个map文件来存储。
  • 在development环境下打包生成的代码不需要做压缩,可以看到里面的说明项,production下一般是被压缩过的代码

optimization

optimization: {
    uesdExports: true // 可以使我们的代码在一些不必要倒入导入的时候可以不导入,比如import '@css/style.css'
}

package.json

{
     // 没有要做按需导入的其他模块,否则要填['style.css','@babel/polly-fill']去配置识别里面的模块
    "sideEffects": false,
}

code splitting 代码分割。可以分成几个文件加载,提高性能

  • 代码分割,和webpack无关
  • webpack中的代码分割可以用2种方式
    • 1、同步方式:可以使用以下代码进行配置,自动帮你代码分割
    optimization: {
        splitChunks: {
            chunks: 'all'
            // 默认值是async,只对异步代码进行代码分割。
        }
     }
    
    • 2、异步方式:import('xxx').then(xxx),无需配置,自动帮你分割。
    • 3、splitChunksPlugin参数讲解
    optimization: {
        splitChunks: {
          chunks: 'async', 
          // 'async'只对异步代码进行代码分割,'all'对同步和异步的代码都进行代码分割,'initial'对同步代码进行代码分割
          minSize: 30000,
          // 引入的代码库大于30kb才做代码分割,小于就不做代码分割。
          maxSize: 0,
          // 配置50000,在大于50000b的时候就可以分成更多的vendors,不过一般不配。
          minChunks: 1,
          // 当一个模块被`引用`至少多少次才能被代码分割,1代表至少被引用1次
          maxAsyncRequests: 5,
          // 所有的分割模块最多能分割出5个js文件
          maxInitialRequests: 3,
          // 入口文件最多只能分割出3个js文件
          automaticNameDelimiter: '~',
          // 组和文件之间连接时用~做连接符
          name: true,
          // 在cacheGroup中起的名字有效
          cacheGroups: {
          // 打包同步代码时,分割的代码放在哪个文件里面
            vendors: {
              test: /[\\/]node_modules[\\/]/,
              // 意思是代码是在node_modules中引入的,就打包一个另一个文件。并在verdors组里,所以生成的文件名叫:vendors-main.js,main就是你的入口名。
              priority: -10,
              // 值越大,优先级越高。
              filename: 'vendors.js'
              // 设置保存的名字就叫vendors不叫vendors-main.js
            },
            default: {
            // 默认走default
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true,
              // 如果一个模块被打包过了,在打包的时候就忽略这个模块。直接使用之前被打包过的模块
              filename: 'common.js'
              // 改名字为common.js,默认放在common.js里面
            }
          }
        }
    }
    

loadsh 是一个高性能使用一次功能函数。可以处理字符串等函数。

Lazy Loading 懒加载

async function getComponent() {
  return import(/* webpackChunkName:"loadsh" */ 'loadsh').then(res => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b', 'c'])
    return element
  })
  // 上面的代码也可以改写成以下代码
  const { default: _ } = await import(/* webpackChunkName:"loadsh" */ 'loadsh')
  const element = document.createElement('div')
  element.innerHTML = _.join(['a', 'b', 'c'])
  return element
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

  • 上面这种方式就是懒加载,在页面刚开始的时候不会加载loadsh这个文件,只有在点击之后才会加载,实现页面的懒加载,使刚加载页面的时候加载速度更快。
  • 懒加载不是webpack的概念,es提出的概念。

chunk是什么?

  • 打包生成的每一个js文件都是chunk

打包分析

preloading、prefetching---code coverge

  • 可以帮助我们在浏览器空闲的时候缓存将来要加载的内容。
  • 使用如下:
document.addEventListener('click', () => {
  import(/* webpackPrefetch: true*/ './click.js').then(({default: func}) => {
    func()
  })
})

coverge分析:
打开chrome控制台,输入command+shift+p健,输入coverage.

css文件的代码分割

  • 使用插件MiniCssExtraPLugin,只能运行在线上环境中。不支持hmr
  • optimize-css-assets-webpack-plugin css代码合并和压缩
  • 在optimization中配置
splitChunks: {
    cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/, 
          chunks: 'all',
          // 对所有同步还是异步加载的css文件,都打包到名为styles的文件中
          enforce: true
          // 忽略掉之前的minSize,maxSize参数的区分。
        }
    }
}

webpack与浏览器缓存

  • 在打包生成的main.xxx.js和vendor.xxx.js中,生成了一个runtime.xxx.js,因为配置了以下内容。
optimization: {
    runtimeChunk: {
        name: 'runtime'
    }
}

在main.js和vendor第三方库之间的关联代码叫manifest

  • 在output中配置contenthash帮助浏览器坐缓存。 代码如下:
output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js'
}

shimming shim 预置依赖,解决打包过程中的兼容性问题。

  • 使用webpack.ProvidePlugin进行配置。来提供全局模块
new webpack.ProvidePlugin({
    $: 'jquery'
})
  • 使用imports-loader 来将每个模块中的this指向全局变量window,使用示例:
module: {
    rules: [{
        test: /\.js$/,
        exclude: /node_modules/,
        use: [{
            loader: 'babel-loader'
        },  {
            loader: 'imports-loader?this=>window'
            // 通过配置,可以使this指向全局变量。
        }]
    }]
}

打包线上环境

webpack.config.js的配置

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  // externals: ["lodash"], // 第一种配置方式
  // externals: {
  //   lodash: {
  //     root: '_', // 通过全局script标签引入,并在页面中注入_为全局变量的lodash
  //     commonjs: 'lodash'
  //   }
  // }, // 第二种配置方式
  external: 'lodash', // 第三种配置方式,通过`import lodash from lodash`去引入
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'library.js',
    library: 'root',
    // 打包生成的代码挂载到全局变量上,可以通过<script src="library"></script>方式去引入,配置root可以通过root变量去引入
    libraryTarget: 'umd'
    // 通过任何方式去引入库,都可以正常引用得到,填this->this.library,window->window.library,global->global.library去引入
  }
}

pwa的打包配置

  • http-server生成一个服务器,然后在本地启动。
  • pwa的作用,第一次访问的时候,有个服务器可以访问,当服务器停掉的时候,可以使用缓存进行处理。
  • 使用pwa的插件:workbox-webpack-plugin
使用示例:
在config文件中配置serviceworker
const WorkboxPlugin = require("workbox-webpack-plugin")
plugins: [
        new WorkboxPlugin.GenerateSW({
          clientsClaim: true,
          skipWaiting: true
        })
    ],
在业务代码中应用serviceworker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('./service-worker.js')
      .then(registration => {
        console.log('service-worker registed')
      }).catch(error => {
        console.log('service-worker register error')
      })
  })
}

这样可以让我们的页面放在缓存中。

Typescript的打包配置

  • typescript是javascript的一个超集
  • 需要额外配置tsconfig.json的文件
{
  "compilerOptions": {
    "outDir": "./dist", // 输出目录
    "module": "es6", // 使用es6
    "target": "es5", // 打包输出es5语法的文件代码类型
    "allowJs": true  // 允许在ts中引入js模块
  }
}
  • 需要使用外部库,同时实现可以提示外部库的一些报错信息。就需要引入外部库的相应ts类型文件
  • 比如:import _ from 'lodash'
  • 就要引入@types/lodash

WebpackDevServer实现请求转发

  • 在mode为development环境下,在devServer下配置proxy进行转发,在线上环境中没有devServer,所以一般不会生效
module.exports = {
    mode: 'development',
    devServer: {
        xxx,
        proxy: { // 在开发过程中做接口转发
			'/react/api': { // 当识别到是这个路径时
				target: 'http://www.dell-lee.com', // 将当前的localhost转发到这这个域名
				pathRewrite: {
					'header.json': 'demo.json'
				}
				// 当header.json路径下还没开发好,可以转发到demo.json这个文件。在开发好后再去掉这个配置
			}
		}
    }
}
  • 当请求是https的时候就要使用secure: false,如以下代码:
module.exports = {
    mode: 'development',
    devServer: {
        xxx,
        proxy: {
			'/react/api': {
				target: 'https://www.dell-lee.com', // 这里使用了https
				secure: false, // 当使用https时,这里就要配置secure,因为默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。
				pathRewrite: {
					'header.json': 'demo.json'
				}
			}
		}
    }
}
  • 使用bypass还可以进行一次拦截。如以下代码:
module.exports = {
    mode: 'development',
    devServer: {
        xxx,
        proxy: {
            index: '', // 默认proxy不支持根目录的转发,所以要使用index进行配置
			'/react/api': {
				target: 'http://www.dell-lee.com',
				context: ['/auth', '/api'], // 如果有多个路径可以这么处理
				bypass: function(req, res, proxyOptions) {
                  if (req.headers.accept.indexOf("html") !== -1) {
                    console.log("Skipping proxy for browser request.");
                    return "/index.html";
                  }
                }
			},
			changeOrigin: true // 可以防止网站对origin的转发进行限制。
			headers: { // 可以对请求的headers进行转发。
			   host: 'www.dell-lee.com',
			   cookie: 'asd'
			}
		}
    }
}

WebpackDevServer 解决单页面应用路由问题

  • devServer中配置:historyApiFallback参数
  • devServer只能在开发环境中使用
  • 代码示例如下:
devServer: {
    historyApiFallback: true
}
// 也可以配置rewrite,如以下:
devServer: {
    historyApiFallback: {
      rewrites: [
        { from: /^\/$/, to: '/views/landing.html' },
        // 遇到以/开头和结尾的文件,直接路由到/views/loading.html文件中
        { from: /^\/subpage/, to: '/views/subpage.html' },
        { from: /./, to: '/views/404.html' }
      ]
    }
}

eslint在webpack中的配置

  • eslint配置在项目中快速初始化可以用:npx eslint --init
  • 使用步骤,在项目中安装eslint->安装eslint-loader->在devServer中配置overlay为true->在module中配置eslint-loader
devServer: {
    overlay: true // 这个配置可以直接在启动时直接告知打包时候遇到的问题 
}
module: {
    rules: [{ 
		test: /\.js$/, 
		exclude: /node_modules/,
		use: ['babel-loader', 'eslint-loader'] // 在这里配置eslint-loader
	}
}
  • 以下可以自动帮你修复代码
module: {
    rules: [{ 
		test: /\.js$/, 
		exclude: /node_modules/,
		use: ['babel-loader', {
		  loader: 'eslint-loader',
		  options: {
		      fix: true, // 这里配置可以自动帮我们配置掉代码格式的问题
		      force: 'pre' // 强制eslint先执行
		  }
		}]
	}
}

提升webpack打包速度

  • 1、跟上技术的迭代,升级webpack/node/npm/yarn的版本
  • 2、在尽可能少的模块上应用loader,比如以下代码:
module: {
    rules: [{ 
		test: /\.js$/, 
		exclude: /node_modules/, // 这里使用exclude不去编译node_module中的代码,因为里面是已经打包编译过的,就可以提高打包编译速度。
		include: path.resolve(__dirname, '../src') // 设置只有src文件夹下才使用babel-loader
		use: [{
		    loader: 'babel-loader'
		}]
}
  • 3、plugin尽可能精简并确保可靠
    • 最好使用官方插件或者社区验证过的插件
    • plugin要尽可能少的使用
  • 4、resolve参数合理配置
// 比如在配置中使用
module.exports = {
    resolve: {
        extensions: ['.js', '.jsx'], // 这里配置的含义是在页面中引用其他组件时,帮助你在那个目录下去首先找js结尾的文件,再去找以jsx结尾的文件。
        mainFiles: ['index', 'child'] // 这里配置的含义是,当引入child/目录时,默认去找child下的index.js文件,找不到再去找child.js文件
        alias: {
            dellee: path.resolve(__dirname, '../src/child') // 这里使用别名,当引用delle这个别名的时候,指向src下的child目录。当写`import child from dellee`去引入第三方库时
        }
        
    }
}

resolve虽然好用,但是不能滥用。要合理配置,不然会降低webpack的打包速度。

  • 5、使用DllPlugin提高打包速度
    • 步骤:
    • (1)、首先使用一个webpack.dll.js,来将第三方插件引入都加载打包为一个xx.dll.js文件
    • (2)、使用DllPlugin来生成一个相应的manifest.json文件,映射到webpack.dll.js文件中。使得我们引用第三方模块的时候,只使用dll文件引入,只引用1次。
    // webpack.dll.js 文件配置
    const path = require('path');
    const webpack = require('webpack');
    
    module.exports = {
    	mode: 'production',
    	entry: {
    		vendors: ['lodash'],
    		react: ['react', 'react-dom'],
    		jquery: ['jquery']
    	},
    	output: {
    		filename: '[name].dll.js',
    		path: path.resolve(__dirname, '../dll'),
    		library: '[name]' // 这个配置可以生成一个全局变量,名字为name
    	},
    	plugins: [ // 使用Dllplugin
    		new webpack.DllPlugin({
    			name: '[name]',
    			path: path.resolve(__dirname, '../dll/[name].manifest.json'),
    		})
    	]
    }
    
    • (3)、在项目的配置中使用DllReferencePlugin来生成xxx.manifest.json文件进行映射。使得项目不去node_modules中使用第三方插件,而去xxx.dll.js文件中去找,从而实现打包速度的提升。
    // webpack.common.js中新增的配置
    plugins: [
        new AddAssetHtmlWebpackplugin({
            filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
        })
        // 这里借助AddAssetHtmlWebpackplugin插件来全局变量vendors挂载到windows中。
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
        })
    ]
    
    • (4)、上面entry中有多个入口名,那就会生成多个xxx.dll.js和xxx.manifest.json文件,从而使得plugin中的配置要多配介个new AddAssetHtmlWebpackplugin和new webpack.DllReferencePlugin.这时就可以使用函数来进行通用处理,从而使配置更灵活些。
    // 代码示例:
    const plugins = [ // 这里是固定的plugin插件
    	new HtmlWebpackPlugin({
    		template: 'src/index.html'
    	}), 
    	new CleanWebpackPlugin(['dist'], {
    		root: path.resolve(__dirname, '../')
    	})
    ];
    
    // 下面通过node模块引入fs文件读取dll文件夹下的内容,然后进行匹配处理,从而达到更加只能。
    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)
    		}))
    	}
    	if(/.*\.manifest.json/.test(file)) {
    		plugins.push(new webpack.DllReferencePlugin({
    			manifest: path.resolve(__dirname, '../dll', file)
    		}))
    	}
    })
    
    module.exports = {
        xxx,
        plugins, // 直接去引入
    }
    
  • 6、控制包文件大小
    • 在项目中不需要的第三方模块要使用tree-shaking去控制或者不去引入,从而避免打包生成的文件太大。
    • 也可以使用splitChunkPlugin插件来对代码进行拆分。
  • 7、webpack是基于node的,所以是单进程的,我们可以使用thread-loader多进程打包,或者paraller-webpack进行多页面打包,happypack 等来进行多进程打包,从而提高打包速度。
  • 8、合理使用sourceMap,因为sourceMap越详细,打包的速度越慢,所以要合理应用,在不同环境打包的时候,使用不同的配置
  • 9、结合stats分析打包结果。
  • 10、开发环境使用内存编译
  • 11、开发环境无用插件剔除

多页面的webpack配置

  • 原理:在entry文件增加多个入口js文件,使用new HtmlWebpackPlugin配置生成多个html,实现多页面打包的流程。。
// 代码示例:
const makePlugins = (configs) => {
    // 这里是plugin的默认配置
	const plugins = [
		new CleanWebpackPlugin(['dist'], {
			root: path.resolve(__dirname, '../')
		})
	];
	
	// 下面通过获取configs中的entry来循环使用htmlWebpackplugin生成不同的文件,从而可以实现打包多页面的功能
	Object.keys(configs.entry).forEach(item => {
		plugins.push(
			new HtmlWebpackPlugin({
				template: 'src/index.html',
				filename: `${item}.html`,
				chunks: ['runtime', 'vendors', item]
			})
		)
	});
	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)
			}))
		}
		if(/.*\.manifest.json/.test(file)) {
			plugins.push(new webpack.DllReferencePlugin({
				manifest: path.resolve(__dirname, '../dll', file)
			}))
		}
	});
	return plugins;
}
const configs = {
    // 这里每使用1个新的页面,新增一个新的入口即可。
    entry: {
		index: './src/index.js',
		list: './src/list.js',
		detail: './src/detail.js',
	}
}
configs.plugins = makePlugins(configs);
module.exports = configs

编写自己的loader

  • 1、什么情况下可以自己编写loader
    • (1)、异常捕获
    • (2)、可以使用loader来进行国际化。

loader和plugin的区别

  • 当需要引用js文件或者其他格式的文件时,可以使用loader去帮助处理这个文件,帮助我们去处理模块
  • plugin在我们打包的时候,就会生效,比如打包生成个html文件,就使用htmlWebpackplugin,当打包之前需要清理文件夹,就使用cleanWebpackPlugin
  • loader本质上一个函数,plugin本质上是1个类

bail

  • 一旦在打包过程中遇到错误就及时停止

pathinfo

  • 会把入口的一些的信息通过注释的形式输出。

devtoolModuleFilenameTemplate

  • 帮助我们找到source-map真正的位置,从而帮助我们更好的调错

optimization

  • 做一些优化和代码压缩

runtimeChunk

runtimeChunk: true // 会把runtime中的代码单独打包成一个文件

vue-cli

  • 基于vue的脚手架工具