一、入坑初探
1. 设置项目为私有
我们只需要在package.json文件中配置,因为是私有项目不需要向外部暴露的,所以我们可以去掉main: index.js
"private": true
2. 运行webpack
一般我们安装webpack时会同时安装webpack-cli,它的作用是使我们可以在命令行使用webpack命令,在命令行中执行
npx webpack --config webpack.config.js
--config指定webpack执行的文件,如果没有,默认是webpack.config.js,因为我们是在命令行中执行,所以需要npx,如果我们写在package.json文件中,则只需要"bundle": "webpack"
就可以了。
3. webpack简单配置
webpack只能识别后缀是.js的文件,如果是其它类型的文件,就需要引入loader来帮助我们编译。
下面我们来做一个简单的对图片和css的打包配置:
rules: [
{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: { // loader额外参数配置
name: '[name]_[hash].[ext]', // name: 原来的名字 ext:原来的后缀
outputPath: 'images/', // 输出路径
limit: 10240 // 限制,大于10240kb时才进行此操作,否则直接打到js文件中
}
}
},{
test: /\.scss$/,
use: [
'style-loader', // 将css挂载到header中
// options: {
// insertAt: 'top' // 插到顶部
// },
'css-loader', // 分析当前有几个css文件,将css文件整合,分析@import这种语法
'sass-loader',
'postcss-loader'
]
}
]
file-loader和url-loader的区别是url-loader会把图片等(任何文件)文件直接打包到js中,如果图片很小,我们可以使用这种方式,如果图片较大,我们就需要将图片打包到统一的images目录中,在上面代码中我们做了一个限制,当图片大于10kb时,就打包到images目录中,否则直接打包到js中
注意loader执行顺序是从下到上执行的,如css这里,执行顺序为:
postcss-loader->sass-loader->css-loader->style-loader
最后我们再来看打包完命令行中的展示,如下图所示:
Chunks: 打包的js的id,
Chunk Names: 打包的js名字
二、loader篇
1. css相关loader
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2, // 如果当前引入的scss文件又引入了其它scss文件,让引入的scss文件也需要通过postcss-loader,sass-loader编译,如果不加,就会直接走css-loader,2代表前两个,几就代表前几个
modules: true // 开启css模块化,开启后css需要用模块化引入的写法
}
},
'sass-loader',
'postcss-loader'
]
}
关于配置css-next的方法查看postcss-loader的文档:https://webpack.js.org/loaders/postcss-loader
2. 打包字体文件
{
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}
打包字体文件用file-loader把字体文件打包到dist目录中就可以了
三、webpack基础
plugins相当于vue,react中的钩子,可以在webpack运行到某个时刻的时候,帮助我们做一些事情
1. html-webpack-plugin
我们需要自动生成一个html文件,把打包生成的js自动引入到这个html文件中
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html' // 指定模版文件
})
2. CleanWebpackPlugin
我们需要在每次打包后删掉上一次的打包文件
new CleanWebpackPlugin(['dist'])]
关于dist目录和webpack配置文件不在同一个根目录下,我们需要如下解决方法
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
2. copyWebpackPlugin
有些时候我们需要拷贝一些静态资源文件到dist目录
new CopyWebpackPlugin([
{from: 'doc', to: './'}
])
2. bannerPlugin
版权声明插件,可以在我们打包生成的文件前生成一些版权信息等
new webpack.BannerPlugin('zxhnext@qq.com')
3. 打包多份js,指定cdn引用路径
首先我们需要配置多入口
entry: {
main: './src/index.js',
sub: './src/index.js'
}
出口处我们不能写死一个名字,否则会因打包处两份相同的文件而报错
output: {
publicPath: 'http://cdn.com.cn', // 设置前缀(cdn地址)
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
4. sourceMap
devtool: 'cheap-module-eval-source-map' // development
devtool: 'cheap-module-source-map' // production
一般在开发环境中我们使用cheap-module-eval-source-map
,在线上环境使用cheap-module-source-map
,如果要关闭sourceMap我们需要把devtool置为none
cheap:1. 只指出哪一行出错,不指出哪一页。2. 只报我们的业务代码,不处理loader等中的代码错误。
module:指出loader等中的错误
source-map: 生成一个.map文件
inline: 将映射文件放到main.js中
eval: 将业务代码与 以及source-map通过eval方式执行,速度最快
具体用法参考官方文档:https://webpack.js.org/configuration/devtool/#devtool
5. 热启动
5.1 通过shell脚本
"watch": "webpack --watch",
我们只需要在package.json文件中设置watch即可,但是这种方法存在很多缺陷,如果我们需要开启一个本地服务,那么我们需要使用webpack-dev-server
5.2. webpack-dev-server
devServer: {
contentBase: './dist',
open: true, // 是否打开浏览器
port: 8080
}
我们需要注意的是,使用webpack-dev-server时我们并未发现有dist目录,这时因为webpack-dev-server将打包好的文件隐藏到计算机的内存中了,这样执行更快。
关于webpack-dev-server的更多配置参考官网:https://webpack.js.org/configuration/dev-server
下面我们来实现一个简单的webpack-dev-server
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
// 编译
const complier = webpack(config);
const app = express();
// 在应用里使用webpack
app.use(webpackDevMiddleware(complier, {
// config.output.publicPath
}));
app.listen(3000, () => {
console.log('server is running');
});
在命令行中使用webpack语法:https://www.webpackjs.com/api/cli/
在node中使用webpack: https://www.webpackjs.com/api/node/
6. Hot Module Replacement 热模块更新
当我们每次修改代码时,页面都会整个刷新,这样岂不是很麻烦,有没有办法只更新被修改的部分,而不刷新整个页面,这时我们需要用到HotModuleReplacementPlugin
const webpack = require('webpack');
module.exports = {
...
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true
},
...
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
这里我们要注意的是,必须在devServer加上hot: true,hotOnly: true
,Hot Module Replacement才会生效
hotOnly: HotModuleReplacementPlugin失效时,重新刷新一次页面
修改了某个文件后,我们就需要手动去更新了
if(module.hot) {
module.hot.accept('./number', () => {
document.body.removeChild(document.getElementById('number'));
number();
})
}
然而我们平常用css,vue和react等的时候并没有这么去做,这是因为是因为css-loader,vue-loader,react-loader中自动帮我们实现了
7. Babel 处理 ES6 语法
这里我们要参考babel官方文档:https://babeljs.io/setup#installation,下面我们先来做一个简单的配置:
{
test: /\.js$/,
exclude: /node_modules/,
// include: path.resolve(__dirname, '../src'), // 只检测某个目录,exclude除掉某个目录
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env', { // @babel/preset-env将es6转为es5
useBuiltIns: 'usage'
}]]
}
}
这里我们需要注意,我们需要配置exclude: /node_modules/, 否则这里也会去匹配node_modules中的js文件,同时我们可以看到,如果所有配置都写在webpack.config.js中,那将会变得非常复杂,所以这里建议新建一个.babelrc文件,将babel-loader中的配置放在.babelrc中,如下所示:
{
presets: [
[
"@babel/preset-env", {
targets: {
chrome: "67", // 支持哪个版本以上的浏览器
},
useBuiltIns: 'usage' // 实现按需加载
}
]
]
}
在有些低版本浏览器中是不支持es5的一些语法的,这时我们需要@babel/polyfill
帮我们解决,我们直接在入口文件中main.js引入@babel/polyfill即可
import "@babel/polyfill";
但是我们怎么实现按需加载呢,我们再.babelrc中添加useBuiltIns: 'usage'
如果配置了useBuiltIns: 'usage'
,会默认引入@babel/polyfill
,不需要手动调用
参见官网:https://babeljs.io/docs/en/babel-polyfill
8. 类库的配置
当我们写一个类库时,我们可以用@babel/plugin-transform-runtime,相比@babel/polyfill,它是通过闭包实现依赖注入,这样做不会污染全局环境
{
"plugins": [["@babel/plugin-transform-runtime", {
"corejs": 2, // 设为2可以实现按需引入而不是全局引入,设为2后需要安装@babel/runtime-corejs2
"helpers": true,
"regenerator": true,
"useESModules": false
}]]
}
9. watch用法
watch: true,
watchOptions: { // 监控的选项
poll: 1000, // 每秒监控多少次
aggregateTimeout: 500, // 防抖,停止输入500ms后再打包
ignored: /node_modules/ // 不需要监控的文件夹
}
四、Webpack进阶
1. Tree-shaking
Tree-shaking大意就是只打包我们有使用的代码,将无用的部分去掉,举例如下:
我们有一个math.js的方法库,内容如下
export const add = (a, b) => {
console.log( a + b );
}
export const minus = (a, b) => {
console.log( a - b );
}
然后我们在index.js中使用math.js的add方法
import { add } from './math.js';
add(1, 7);
这里有一点我们需要注意,Tree-shaking只支持import这种ES Module,不支持require这种形式的。
虽然我们只引入了add方法,但是webpck默认把math.js中所有的文件都帮我们打包了,如何做到只打包我们使用的部分代码呢?这时我们需要在webpack中作如下配置
plugins: [],
...
optimization: {
usedExports: true
},
然后我们需要在package.json文件中这样配置:
"sideEffects": [ // 不对下面的文件进行tree shaking
"@babel/polly-fill",
"*.css"
]
首先来解释下它是什么意思,即忽略掉哪些模块不做Tree-shaking,首先我们要忽略所有的css文件,其次如果像import @babel/polyfill
这种形式的,我们没有引入任何东西,webpack会自动帮我们忽略掉,这样打包文件就出错了
在生产环境tree shaking 是自动生效的,不用再webpack中做配置,但是我们依然需要在package.json中需要配置
"sideEffects": false // false代表没有需要忽略的文件
2. Develoment 和 Production
我们仿照create-react-app,创建build目录存放我们的weback配置文件,首先我们将公用文件提到webpack.common.js,然后我们用webpack-merge合并,如下所示:
const commonConfig = require('./webpack.common.js');
const devConfig = {
...
}
module.exports = merge(commonConfig, devConfig);
因为我们将webpack配置文件放在了build目录中,此时dist与webpack配置文件不在同一根目录下,这是我们需要解决dist和webpack不在同一个根目录下而产生的clean插件无法删除dist目录问题,解决方法如下:
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
3. Code Splitting代码分割
3.1 多入口打包方法
如果我们想把引入的模块单独打包,我们需要单独创建一个文件引入这个包,然后挂载到window上,再在入口处引入这个文件
这里我们以lodash为例:
新建lodash.js文件,内容如下:
import _ from 'lodash';
window._ = _;
然后我们在entry引入这个包,
entry: {
lodash: './src/lodash.js',
main: './src/index.js'
}
3.2 配置optimization
在webpack中我们可以配置chunks来自动帮我们做(同步)代码分割
optimization: {
splitChunks: {
chunks: 'all'
}
}
3.3 异步模块打包
异步模块不需要我们做任何配置,webpack会自动帮我们将异步代码打包到另一个文件中。
在使用异步加载的写法时(vue中懒加载模块),我们需要安装@babel/plugin-syntax-dynamic-import
,然后在.babelrc中配置
{
presets: [
[
"@babel/preset-env", {
targets: {
chrome: "67",
},
useBuiltIns: 'usage'
}
]
],
plugins: ["@babel/plugin-syntax-dynamic-import"]
}
异步代码写法
function getComponent() {
return import('lodash').then(({ default: _ }) => {
var element = document.createElement('div');
element.innerHTML = _.join(['Dell', 'Lee'], '-');
return element;
})
}
getComponent().then(element => {
document.body.appendChild(element);
});
// es7写法
async function getComponent() {
const { default: _ } = await import(/* webpackChunkName:"lodash" */ 'lodash');
const element = document.createElement('div');
element.innerHTML = _.join(['Dell', 'Lee'], '-');
return element;
}
document.addEventListener('click', () =>{
getComponent().then(element => {
document.body.appendChild(element);
});
})
import(/* webpackChunkName:"lodash" */ 'lodash');
这是魔法注释,加上后,打包出来的js会是你注释的值,否则为一个id(如0)值
4. SplitChunksPlugin 配置参数
splitChunks默认配置,当我们写一个splitChunks: {}
,默认等于如下
splitChunks: {
chunks: "async", // async 只对异步代码生效, all同步异步都生效, initial同步生效
minSize: 30000, // 文件大于多少时才会打包
//maxSize: 0, // 会尝试对大于多少的文件再次分割为两个小文件
minChunks: 1, // 当一个模块至少被用了几次后才做代码分割
maxAsyncRequests: 5, // 最多分割几个包
maxInitialRequests: 3, // 入口文件引入的库最多能分割成几个包
automaticNameDelimiter: '~', // 生成文件名字中间的连接符
name: true, // 使cacheGroups中设置的文件名有效
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10 // 优先级的意思,如果同时满足vendors和default,这个值谁大就打包到哪个组,-10大于-20
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 如果引入的某个文件之前已经被打包过,就不会被打包了,会直接去复用之前的
}
}
}
chunks: "all"时我们需要注意,这时webpack会继续找到cacheGroups
,vendors中的test表示被打包的文件是否在node_modules这个文件夹中,如果是的话,就会打包到vendors这个组中,这时打包出来的文件名字应该是vendors~main.js
,main是定义的入口文件名字,如果我们想指定一个名字,可以在vendors中设置filename指定一个名字
default是指如果不符合vendors中的要求的文件,比如我们自己写的一个包,这个包并不在node_modules中,这时会分到default组中
cacheGroups作用是做一个缓存组,如果我们引入了多个包,就会分割成很多模块,而cacheGroups作用就是先将需要打包的文件缓存起来,然后统一打包到一个组中
vendors, default也可以设置为false
5. 打包分析,Preloading, Prefetching
5.1 打包分析
在package.json中设置一个下面的命令,然后运行
"dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js"
会生成一个stats.json文件,这是一个对打包过程的描述文件,借助一些工具我们可以进行分析。
参考analyse:https://github.com/webpack/analyse
参考官网:https://webpack.js.org/guides/code-splitting/#bundle-analysis
5.2 代码使用率
在浏览器调试工具中按command+shift+p
,然后我们选择show coverage选项,可以查看代码的使用率,代码使用率越高说明优化的越好,所以我们开发时尽量多写异步的代码,这样代码使用的时候才会去加载
如下所示:
// click.js
function handleClick() {
const element = document.createElement('div');
element.innerHTML = 'Dell Lee';
document.body.appendChild(element);
}
export default handleClick;
// index.js
document.addEventListener('click', () =>{
import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => {
func();
})
});
Prefetching是等主代码加载完才会加载,Preloading是与主代码同时加载
6. CSS代码分割
这里我们需要使用mini-css-extract-plugin
参考官网:https://webpack.js.org/plugins/mini-css-extract-plugin
6.1 我们先来看一下output内容:
output: {
filename: '[name].js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, '../dist')
}
这里说一下filename与chunkFilename的区别:
入口文件的打包用filename,chunk文件打包用chunkFilename
6.2 分割css
如果我们不分割css,webpack会默认把css打包到js文件中,这是我们不希望看到的,下面来看下mini-css-extract-plugin
的使用方法。注意,如果打包失败,需要看一下是不是package.json文件中这里配置有误,可能是tree shaking影响了
"sideEffects": "false"
// 改为
"sideEffects": ["*.css"]
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
}
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css'
})
]
6.3 css压缩
我们还可以对css进行压缩,这时我们需要用到optimize-css-assets-webpack-plugin
然后配置如下:
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
}
6.4 多入口的css打包到一个css中
这个配置意思是只要是css文件就打包到这个组中
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true
}
}
}
}
enforce为true表示忽略其它的默认参数
6.5 不同入口打包到不同组
参考官网:https://webpack.js.org/plugins/mini-css-extract-plugin
6.6 去掉性能上的警告
performance: false, // 去掉性能上的警告
output: {
path: path.resolve(__dirname, '../dist')
}
7. runtimeChunk
配置runtimeChunk是因为在一些老版本的webpack中,manifest(包与包之间的关系)文件是加在main与vendors文件中的,这样会导致即使我们没有更改文件,但是包与包之间的关系变了而引起的contenthash发生变化,这时我们就需要这样配置将这部份代码抽离出来,在新版webpack中不会出现这个问题
optimization: {
runtimeChunk: {
name: 'runtime'
},
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
}
}
}
},
8. Shimming
一些第三方的库(library)可能会引用一些全局依赖(例如 jQuery 中的 $)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming 发挥作用的地方。
8.1 全局引入
new webpack.ProvidePlugin({
$: 'jquery',
_join: ['lodash', 'join']
})
当发现一个模块中用了$时,会在模块中默认引入jquery
如果需要使用模块中的某个方法,我们可以用一个数组的方式定义
8.2 修改this指向
每个模块的this指向的都是模块自身,如果想让this指向window,需要imports-loader插件,然后我们再做如下配置:
{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader'
}, {
loader: 'imports-loader?this=>window'
}
9. 环境变量的使用
module.exports = (env) => {
if(env && env.production) {
return merge(commonConfig, prodConfig);
}else {
return merge(commonConfig, devConfig);
}
}
然后在package.json中设置环境变量
"build": "webpack --env.production --config ./build/webpack.common.js"
五、webpack高级使用技巧
1. 类库代码打包
我们对package.json进行设置
"license": "MIT", // 开源
然后在output中做如下设置:
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'library.js',
library: 'library', // 可以script标签引入,在全局挂载了一个library变量
libraryTarget: 'umd' // 使支持amd,cmd,require等语法
}
还可以用如下写法
library: 'library', // 可以script标签引入,在全局挂载了一个library变量
libraryTarget: this' // 这两个配合就不支持amd等写法了,只会挂载一个全局变量
lodash : {
commonjs: 'lodash', // 通过require(common.js)引入时,名字必须叫lodash
amd: 'lodash',
root: '_' // 通过script标签引入时必须在全局挂载一个_变量
}
const lodash = require('lodash') // commonjs设置的意思是const后的名字必须叫lodash
如果我们编写的库中引入了其它包,我们不希望引入的包被打包,这时我们可以设置
module.exports = {
...
externals: 'lodash',
output: {
...
}
}
这里写成一个数组,对象,字符串形式都可以,对象形式:
module.exports = {
...
externals: {
lodash: {
commonjs: 'lodash'
}
},
output: {
...
}
}
参考官网:https://webpack.js.org/configuration/externals/#externals
最后我们需要把package.json的入口文件改为
"main": "./dist/library.js",
然后在npm注册一个账号,
然后npm adduser添加用户名和密码
再npm publish
2. PWA 的打包配置
安装workbox-webpack-plugin
在plugins中配置:
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
js文件为
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');
})
})
}
3. TypeScript 的打包配置
我们需要安装ts-loader typescript
在rules中配置:
{
test: /\.tsx?$/, // ?代表可有可无
use: 'ts-loader',
exclude: /node_modules/
}
同时创建tsconfig.json文件,做如下配置
{
"compilerOpitons": {
"outDir": "./dist",
"module": "es6", // 使用es6的模块引入方法
"target": "es5", // 转换为es5形式
"allowJs": true // 允许ts中引入js文件
}
}
一般库的typescript版本都是@types/名字,可以参考:https://github.com/DefinitelyTyped/DefinitelyTyped
4. WebpackDevServer 实现请求转发
注意本章只在开发环境生效,对生产环境没有影响
4. 1. 代理接口
devServer: {
proxy: {
// index: '', // 如果要代理根路径,需要把index设置为false或者''
'/react/api': {
target: 'https://www.dell-lee.com', // 代理请求接口
secure: false, // 如果是https网址,这里需要设置为false
pathRewrite: { // 代理接口,访问header.json时会帮你请求demo.json
'header.json': 'demo.json'
},
changeOrigin: true, // 后端可能设置了changeOrigin防止爬虫,这里我们设置true以后就可以避开这个限制了
headers: { // 设置请求头
host: 'www.dell-lee.com',
cookie: ....
},
bypass: function(req, res, proxyOptions) { // 拦截,如果请求的是一个html内容,则返回index.html
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.');
return '/index.html';
}
}
}
}
}
webpackdevserver proxy底层用了 http-proxy-middleware这个插件
如何使用mock数据
devServer: {
before(app) {
app.get('/user', (req, res) => {
res.json(....)
})
}
}
4. 2. WebpackDevServer 解决单页面应用路由问题
当不使用hash路由时,我们可以设置以下内容
historyApiFallback: true, // 把对服务器的请求都转换为对跟路径的请求
historyApiFallback: {
rewrites: [ // 访问abc.html时代理到index.html
{ from: /abc.html/, to: '/views/index.html' }
]
}
historyApiFallback: true相当于
historyApiFallback: {
rewrites: [
{ from: /\.*\/, to: '/index.html' }
]
}
底层用了connect-history-api-fallback这个插件
5. EsLint 在 Webpack 中的配置
安装eslint
npx eslint --init
module.exports = {
"extends": "airbnb", // 使用那个规则
"parser": "babel-eslint", // 解析器
"rules": {
"react/prefer-stateless-function": 0,
"react/jsx-filename-extension": 0
},
globals: {
document: false // 不允许覆盖全局变量document
}
};
在webpack中使用eslint
安装eslint-loader:https://webpack.js.org/loaders/eslint-loader
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader']
}
同时配置overlay: true
,eslint有错会在浏览器中提示
devServer: {
overlay: true
}
设置force为pre代表强制先执行,fix会自动修复一些项目中eslint简单的错误
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'eslint-loader',
options: {
fix: true
},
force: 'pre'
},
'babel-loader'
]
}
六、webpack 性能优化
1. 经常更新版本
2. 使用loader时指定检测目录,图片没有必要
{
test: /\.js$/,
include: path.resolve(__dirname, '../src'), // 只检测某个目录,exclude除掉某个目录
use: [{
loader: 'babel-loader'
}]
}
3. 尽少使用plugin,尽可能精简并确保可靠
4. 合理配置resolve
resolve: {
extensions: ['.js', '.jsx'],
mainFIles: ['index', 'child']
},
当一个引入的文件没有后缀时,会识别它是不是.js,.jsx文件
引入一个目录,回去查找目录下是否有index,child文件
给文件或路径设置别名
resolve: {
extensions: ['.js', '.jsx'],
alias: {
child: path.resolve(__dirname, '../src/child')
}
},
5. 第三方模块只打包一次
新建一个webpack.dll.js
, 运行它对第三方模块单独打包,并生成vendors.manifest.json
映射文件
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]' // 将它暴露出去
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json'),
})
]
}
然后再配置webpack.common.js
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
new webpack.DllReferencePlugin({ // 查找vendors.manifest.json,如果发现这里有,就不会再重复打这个包
manifest: path.resolve(__dirname, '../dll', '../dll/vendors.manifest.json')
})
new AddAssetHtmlWebpackPlugin({ // 向html中添加引入某个文件
filepath: path.resolve(__dirname, '../dll', '../dll/vendors.dll.js')
})
自动化引入
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)
}))
}
})
6. 控制包文件大小
7. thread-loader,parallel-webpack,happypack多线程打包
let happypack = require('happypack');
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
include: path.resolve('src'),
use: 'happypack/loader?id=js'
}
]
plugins: [
new happypack({
id: 'js',
use: [{
loader: 'babel-loader',
options: {
...
}
}]
})
]
8. 合理使用sourceMap
9. 开发环境内存编译
webpackdevserver用的就是内存编译
10. 开发环境无用插件剔除
11. noParse
module: {
noParse: /jquery/, 不去解析jquery中的依赖库
}
12. ignoreplugin
忽略掉我们不需要引入的包文件中的部分内容
// 我们不需要引入moment这个包里的/locale文件夹,就把它忽略掉
new webpack.IgnorePlugin(/\.\/locale/,/moment/)
七、多页面打包配置
多入口
entry: {
index: './src/index.js',
list: './src/list.js',
detail: './src/detail.js',
}
生成多个html
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'index.html',
chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}),
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'list.html',
chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}),
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'detail.html',
chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}),
自动化方式
const makePlugins = (configs) => {
const plugins = [
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
];
Object.keys(configs.entry).forEach(item => {
plugins.push(
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: `${item}.html`,
chunks: ['runtime', 'vendors', item]
})
)
});
return plugins;
}
八、webpack原理篇
1. 编写一个 Loader
1.1 同步操作
新建loader文件夹,在文件夹中新建replaceLoader.js文件
const loaderUtils = require('loader-utils');
module.exports = function(source) { // 注意这里不能使用箭头函数,我们需要变更this指向来调用this中的一些方法
return source.replace('lee', 'world');
}
然后我们在webpack.config.js中引入
rules: [{
test: /\.js/,
use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
}]
同时我们还可以传入一些参数
rules: [{
test: /\.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'zxh'
}
}
]
}]
这时我们就可以在replaceLoader.js,通过this.query可以接收到options中的内容
module.exports = function(source) {
return source.replace('hello', this.query.name);
}
或者我们可以通过webpack官方提供的loader-utils模块,使用方法如下
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
const result = source.replace('dell', options.name);
return source.replace('hello', options.name);
}
想要返回多个值时可以用this.callback
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const result = source.replace('dell', options.name);
this.callback(null, result, source, mata)
}
1.2 使用异步操作 this.async
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
const callback = this.async(); // 声明是异步操作
setTimeout(() => {
const result = source.replace('dell', options.name);
callback(null, result);
}, 1000);
}
引入模块时,会来node_modules中找,找不到了再来loaders文件夹中找,这时我们就可以像引入node_modules中的loader那样写了
entry: {
main: './src/index.js'
},
resolveLoader: {
modules: ['node_modules', './loaders']
},
module: {
rules: [{
test: /\.js/,
use: [
{
loader: 'replaceLoader',
}
]
}]
}
2. 编写一个 Plugin
发布,订阅设计模式
https://webpack.js.org/api/compiler-hooks
class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => { // 同步,不用传callback
console.log('compiler');
})
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { // emit是异步的,我们需要在后面写tapAsync,打包完放到文件夹时,compiler是所有打包文件,compilation是本次打包文件
debugger;
compilation.assets['copyright.txt']= {
source: function() { // 内容
return 'copyright by dell lee'
},
size: function() { // 文件长度
return 21;
}
};
cb(); // 最后必须调一下cb()
})
}
}
module.exports = CopyrightWebpackPlugin;
开启node调试工具
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"
3. Bundler源码编写
安装cli-highlight:命令行高亮显示工具
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser'); // 帮助分析源代码
const traverse = require('@babel/traverse').default; // 帮助遍历module
const babel = require('@babel/core');
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8'); // 读取文件内容
const ast = parser.parse(content, { // 抽象语法树,ast
sourceType: 'module' // 如果是es6模块方法,这里需要设置
});
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, { // 将ast抽象语法树转换为浏览器可以识别的代码
presets: ["@babel/preset-env"]
});
return {
filename,
dependencies,
code
}
}
const moduleInfo = moduleAnalyser('./src/index.js'); // 入口文件
console.log(moduleInfo);
vue-cli3多页面配置
参考:https://cli.vuejs.org/zh/config/
webpack loader与plugins编写:https://www.jianshu.com/p/21cbc228d7f5