webpack配置方式及如何优化
entry & output
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| module.exports = { entry: './index.js' entry: { main: './index.js' },
output: { filename: 'bundle.js', path: path.resolve(__dirname, 'src') },
entry: { main: './index.js', sub: './index.js', }, output: { filename: '[name].js', path: path.resolve(__dirname, 'src') },
output: { publicPath: 'http://cdn.com/', filename: '[name].js', path: path.resolve(__dirname, 'src') }, }
|
source-map
源代码和生成代码的映射
webpack.config.js
1 2 3 4 5 6 7 8
|
module.exports = { devtool: 'source-map', }
|
最佳实践:
- 开发环境:
cheap-module-eval-source-map
- 线上:
cheap-module-source-map
loader
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| const path = require('path')
module.exports = { mode: 'production' mode: 'development'
module: { rules: [ { test: /\.jpg$/, use: { loader: 'file-loader', options: { name: '[name]_[hash].[ext]', outputPath: 'img/' } } }, { test: /\.jpg$/, use: { loader: 'url-loader', options: { name: '[name]_[hash].[ext]', outputPath: 'img/', limit: 2048 } } }, { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 2, modules: true } }, 'sass-loader', 'postcss-loader' ] }, { test: /\.(eot|ttf|svg)$/, use: [ 'file-loader' ] }, ] } }
|
plugin
npm i html-webpack-plugin -D
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const HtmlWebpackPlugin = require('html-webpack-plugin') const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = { plugin: [ new HtmlWebpackPlugin({ template: 'src/index.html', }), new CleanWebpackPlugin(['dist'])
] }
|
script命令
package.json
1 2 3
| script: { "bundle": "webpack" }
|
postcss-config.js
当webpack配置用到时,需要此配置文件
1 2 3 4 5
| module.exports = { plugins: [ require('autoprefixer') ] }
|
监听文件变化
简单方式
package.json
1 2 3
| "scripts": { "watch": "webpack --watch" }
|
一般方式
npm i webpack-dev-server -D
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { devServer: { contentBase: './dist', open: true, proxy: { '/api': 'http://loaclhost:3000' } } }
|
package.json
1 2 3
| "scripts": { "start": "webpack-dev-server" }
|
热模块更新 HMR
改变样式代码时只改变样式,不刷新页面
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12
| const webpack = require('webpack')
module.exports = { devServer: { hot: true, hotOnly: true }, plugins: [ new webpack.HotModuleReplacementPlugin() ] }
|
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| import a from './a' import b from './b'
a() b()
if (module.hot) { module.hot.accept('./b', () => { b() }) }
|
处理es6 babel
npm i --save-dev babel-loader @babel/core
npm install @babel/preset-env --save-dev
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", options: { presets: [["@babel/preset-env", { targets: { chrome: "67" }, useBuiltIns: 'usage' }]] } } ] } }
|
babel polifill
将Promise转es5
npm install --save @babel/polyfile
如果规则放在 babelrc 内则不用在项目中写
index.js
1
| import "@babel/polyfill";
|
组件库问题
写组件库的话会污染全局变量
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
npm install --save @babel/runtime-corejs2
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", options: { "plugins": [ [ "@babel/plugin-transform-runtime", { "absoluteRuntime": false, "corejs": 2, "helpers": true, "regenerator": true, "useESModules": false } ] ] } } ] } }
|
react
npm install --save-dev @babel/preset-react
npm i react react-dom --save
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", options: { presets: [["@babel/preset-env", { targets: { chrome: "67" }, useBuiltIns: 'usage' }], '@babel/preset-react' ] } } ] } }
|
Tree Shaking
打包时模块内没用到的东西去掉
只支持ES Module方式 (import)
webpack.config.js
1 2 3 4 5 6 7 8
| module.exports = { module: { optimization: { usedExports: true } } }
|
package.json
1 2 3 4 5 6
| "sideEffects": ["@babel/polyfile"]
"sideEffects": ["*.css"]
"sideEffects": false
|
分环境打包
将webpack.dev.js
拷贝一份webpack.pro.js
,对webpack.pro.js
重新配置
package.json
1 2 3 4
| "script": { "dev": "webpack-dev-server --config webpack.dev.js", "build": "webpack --config webpack.pro.js", }
|
抽离通用的配置项
npm i wepack-merge -D
webpack.common.js
webpack.dev.js
1 2 3 4 5
| const merge = require('webpack-merge') const commonConfig = require(./webpack.common.js)
const devConfig = {} module.exports = merge(commonConfig, devConfig)
|
Code Splitting代码分割
假设要引入lodash,不分割会导致整个库打包至main.js
拆分后,业务逻辑变更插件不会重新加载
第一种方式
webpack
1 2 3
| entry: { lodash: './src/lodash.js' }
|
src/loadsh.js
1 2
| import _ from 'lodash' window._ = _
|
第二种方式
自动做分割
src/index.js
webpack
1 2 3 4 5
| optimization: { splitChunks: { chunks: 'all' } }
|
异步加载
实验性语法,babel转换
npm install babel-plugin-dynamic-import-webpack --save-dev
.babelrc
1
| plugins: ["babel-plugin-dynamic-import-webpack"]
|
index.js
1 2 3 4 5
| function getComponents() { return import('lodash').then(({default: _}) => { }) }
|
splitChunksPlugin
index.js
1 2 3 4 5 6
| function getComponents() { return import( 'lodash').then(({default: _}) => { }) }
|
npm uninstall babel-plugin-dynamic-import-webpack
npm i @babel/plugin-syntax-dynamic-import --save-dev
- 使打包的文件前缀无vendors
webpack
1 2 3 4 5 6 7 8 9
| optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendors: false, default: false, } } }
|
splitChunks默认选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| splitChunks: { chunks: "async", chunks: 'all', chunks: 'initial', minSize: 30000,
maxSize: 50000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10, filename: 'vendors.js', }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } } }
|
LazyLoading懒加载
使用import('xxx')
加载的代码,加载时间不固定,具体看触发时机
1 2 3
| function getComponents() { return import('lodash').then() }
|
打包分析
analyse
package.json
1 2 3 4 5 6
| "script": { "dev-build": "webpack --config .....", "//": "新加", "//": "打包过程中,把打包的描述添加到stats里", "dev-build": "webpack --profile --json > stats.json --config .....", }
|
上传文件到
webpack.github.io/analyse/
可以看到分析
其他分析工具
https://webpack.js.org/guides/code-splitting/#bundle-analysis
webpack打包推荐JS写法
以前的写法
1 2 3 4
| document.addEventListener('click', () => { })
|
推荐写法
index.js
1 2 3 4 5
| document.addEventListener('click', () => { import('./click.js').then(func => { func() }) })
|
click.js
1
| export default function handleClick() {}
|
查看代码复用率
控制台 -> Sources -> ctrl+shift+p -> 输入coverage
https://zhuanlan.zhihu.com/p/26281581
preloading\prefetching
https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules
等核心代码加载完成后,页面空闲时加载异步代码
1 2 3
| import( './click.js').then(func => { func() })
|
异步文件命名
webpack.common.js
1 2 3 4 5 6 7 8 9 10 11
| entry: { main: './src/index.js' }
output: { filename: '[name],js', chunkFilename: '[name].chunk.js', path: path.resolve(__dirname, '../dist') }
|
css代码分割
https://webpack.js.org/plugins/mini-css-extract-plugin/
此插件可以对多入口文件打包,具体看官网文档
不对css做Tree Shaking
package.json
1 2 3 4 5
| { "sideEffects": [ "*.css" ] }
|
wepack.common.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
optimization: { minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], },
module: { rules: [ { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../', hmr: process.env.NODE_ENV === 'development', }, }, 'css-loader', ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css', }), ],
|
浏览器缓存
文件源码不变,文件名不会变
webpack.prod.js
1 2 3 4
| output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js' }
|
webpack3以下可能出现每次打包即使源码不变,hash也会变
webpack.common.js
1 2 3 4 5
| optimization: { runtimeChunk: { name: 'runtime' } }
|
shimming 垫片
模块化导致写的模块引入的库可能不在package里
这里加入配置,可以自动加载
webpack.common.js
1 2 3 4 5 6 7 8 9 10
| import webpack from 'webpack'
plugins: [ new webpack.ProvidePlugin({ $: 'jquery', _join: ['lodash', 'join'] }) ]
|
模块
改变模块的this
npm i imports-loader --save-dev
webpack.common.js
1 2 3 4 5 6 7 8 9 10 11 12
| rules: [ { test: /\.js$/, use: [{ loader: 'babel-loader', }, { loader: 'imports-loader?this=>window' }], } ]
|
环境变量
webpack.common.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const merge = require('webpack-merge') const devConfig = require('./webpack.dev.js') const proConfig = require('./webpack.pro.js')
const commonConfig = {}
module.exports = (env) => { if (env && env.production) { return merge(commonConfig, proConfig) } else { return merge(commonConfig, devConfig) } }
|
在打包命令里添加属性
package.json
1 2 3
| "scripts": { "build": "webpack --env.production --config" }
|
也可以写
webpack --env.production=abc --config
这样的话环境变量就有值
env.production === abc
开发框架的打包方式
library
npm init
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "name": "library", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "webpack" }, "author": "", "license": "MIT", "dependencies": { "webpack": "^4.35.2", "webpack-cli": "^3.3.5" } }
|
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const path = require('path')
module.exports = { mode: 'production', entry: './src/index.js', externals: ["lodash"],
externals: { lodash: { commonjs: 'lodash', root: '_' } },
output: { path: path.resolve(__dirname, 'dist'), filename: 'library.js', library: 'library', libraryTarget: 'umd', libraryTarget: 'this', } }
|
/src
math.js
1
| export function add(a, b) { return a + b }
|
index.js
1 2 3
| import * as math from './math'
export default { math }
|
PWA
就算开启的http服务关掉,也能正常访问
npm install workbox-webpack-plugin --save-dev
webpack.prod.js
1 2 3 4 5 6
| plugins: [ new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true }) ]
|
index.js
1 2 3 4 5 6 7
| if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js').then(registration => { console.log('service-worker registed') }) }) }
|
dev - server
http请求代理
webpack.config.js
1 2 3 4 5 6
| devServer: { proxy: { '/react/api': 'http://xxx.com' } }
|
如果测试为请求a.json,实际上线时需要请求b.json
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| devServer: { proxy: { '/react/api': { target: 'http://xxx.com', secure: false, pathRewrite: { 'a.json': 'b.json' }, changeOrigin: true, headers: { host: '', cookie: '' } } } }
|
单页面路由问题
如果不用router类的插件,访问a.com/b则会向后端请求此页面,从而找不到页面。
webpack.config.js
1 2 3
| devServer: { historyApiFallback: true }
|
提高 webpack 打包速度
- 更新webpack、node版本
- 在尽可能少的模块上应用loader
- plugin尽可能精简,并确保可靠性
- resolve参数
- webpack.common.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| resolve: { extensions: ['.js', '.jsx'], mainFiles: ['index'], alias: { components: path.resolve(__dirname, '../src/components') } }
|
- DLLPlugin
webpack.dll.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const path = require('path') const webpack = require('webpack')
module.exports = { mode: 'production', entry: { vendors: ['react', 'react-dom'] }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, '../dll') library: '[name]', }, plugins: [ new webpack.DllPlugin({ name: '[name]', path: path.resolv(__dirname, '../dll/[name].manifest.json') }) ] }
|
package.json
1 2 3
| "scripts": { "build:dll": "webpack --config ./build/webpack.dll.js" }
|
npm i add-asset-html-webpack-plugin --save
webpack.common.js
1 2 3 4 5 6 7 8 9 10
| plugin: [ new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dll/vendors.dll.js') }), new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dll/vendors.manifest.json') }) ]
|
打包出的dll会在全局变量vendors里,其他代码从此变量获取
当模块很多时,可能会有多个dll文件
webpack.common.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const plugins = [] const fs = require('fs')
const files = fs.reddirSync(path.resolve(__dirname, '../dll'))
files.forEach(file => { if (/.*\.dll\.js/.test(file)) { plugins.push( new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dll' + file) }) ) }
if (/.*\.mainfest\.json/.test(file)) { plugins.push( new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dll' + file) }) ) } })
|
控制包文件大小
对代码拆分,或去除不用的模块
多进程打包
- thread-loader
- parallel-webpack
- happypack
sourceMap
分环境打包
结合stats分析打包
webpack自带
编写Loader
这里的例子是替换字指定字符串中的文字
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const path = require('path')
module.exports = { mode: 'development', entry: { main: './src/index.js' }, resolveLoader: { modules: ['node_modules', './loaders'] }, module: { rules: [{ test: /\.js/, use: [ { loader: 'replaceLoader', options: { name: 'naaaame' } } ] }] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
|
/src/index.js
/loaders/replaceLoader.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| const loaderUtils = require('loader-utils')
module.exports = function (source) { const options = loaderUtils.getOptions(this) const callback = this.async()
setTimeout(() => { const result = source.replace('a', options.name) callback(null, result) }) }
|
更多配置查看官方文档
编写Plugin
这个例子是在打包最后放置文件时,创建一个新的文件
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const path = require('path') const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')
module.exports = { mode: 'development', entry: { main: './src/index.js' }, plugins: [ new CopyrightWebpackPlugin({ name: 'naaaame' }) ], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
|
/plugins/copyright-webpack-plugin.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class CopyrightWebpackPlugin { constructor(options) { console.log(options); }
apply(compiler) {
compiler.hooks.emit.tap('CopyrightWebpackPlugin', () => { })
debugger
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { compilation.assets['copyright.txt'] = { source: function() { return '文件内容' }, size: function () { return 12 } } cb() }) } }
module.exports = CopyrightWebpackPlugin
|
package.json
1 2 3 4
| "scripts": { "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js", "build": "webpack" },
|
写一个类似Webpack的工具
依赖包
yarn add @babel/parser
yarn add @babel/traverse
yarn add @babel/core
yarn add @babel/preset-env
文件结构
- /src/index.js
- /src/message.js
- /src/word.js
- /bundler.js
文件内容
/src/index.js
1 2
| import message from './message.js'; console.log(message);
|
/src/message.js
1 2 3
| import { word } from './word.js'; const message = `say ${word}` export default message
|
/src/word.js
1 2
| const word = 'hello' export { word }
|
编译文件 /bundler.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core')
const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, { sourceType: 'module' })
const dependencies = {}
traverse(ast, { ImportDeclaration({ node }) { const dirname = path.dirname(filename) const newFile = './' + path.join(dirname, node.source.value) dependencies[node.source.value] = newFile } })
const { code } = babel.transformFromAst(ast, null, { presets: ["@babel/preset-env"],
})
return { filename, dependencies, code } }
const makeDependenciesGraph = entry => { const entryModule = moduleAnalyser(entry) const graphArray = [entryModule]
for (let i = 0; i < graphArray.length; i++) { const item = graphArray[i]; const { dependencies } = item if (dependencies) {
for (const key in dependencies) { if (dependencies.hasOwnProperty(key)) { graphArray.push(moduleAnalyser(dependencies[key])) } } } }
const graph = {} graphArray.forEach(item => { const { dependencies, code } = item graph[item.filename] = { dependencies, code } }) return graph }
const generateCode = entry => { const graph = JSON.stringify(makeDependenciesGraph(entry)) return ` (function(graph) { function require(module) { // 相对路径转换 function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]) }
// 记录导出的结果 var exports = {}
function init(require, exports, code) { // 重写内部的require,返回绝对路径 eval(code) } init(localRequire, exports, graph[module].code)
return exports } require('${entry}')
})(${graph}) ` }
const code = generateCode('./src/index.js')
eval(code)
|
输出
node .\bundler.js
输出为解析index.js
文件后的代码,浏览器可直接执行