webpack学习计划来啦
邵预鸿 Lv5

开始

webpack学习计划终于来了

一直感觉学webpack有些困难,拖了好久,面试了头条和腾讯都同时问到了webpack,webpack也该提上日程了


安装

1
npm i webpack webpack-cli -D

package.json添加

1
2
3
4
5
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.config.prod.js --env production --progress",
"start": "webpack serve --config webpack.config.env.js"
},

相关文档:https://www.webpackjs.com/guides/installation/#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6

常用名称

  • ‘[name]’ 当前文件名称
  • ‘[contenthash]’ 用于缓存问题,根据内容生成hash名称
  • ‘[ext]’ 文件后缀名
  • ‘[hash]’ 随机hash生成

入口Entry

  • 单个入口文件

    1
    2
    3
    {
    entry:'./app.js'
    }
  • 多个入口文件

    1
    2
    3
    4
    5
    6
    7
    {
    entry: {
    home: './home.js',
    about: './about.js',
    contact: './contact.js',
    },
    }

出口output

1
2
3
4
5
6
7
8
{
output:{
path: path.resolve(__dirname, 'dist'), //打包路径
filename: 'entry.js', //打包生成的文件
clean:true //打包前清除上一次的打包文件
publicPath:'/static' //打包后html文件里,添加公共的路径 如src = /static/app.js
}
}

懒加载

使用import语法,可以实现打包后项目需要的时候再加载,语法为import(‘模块名称’).then({deafult:模块名称}=>{})

  • import(‘/ * webpackChunkName: “jqueryA” * /‘) webpackChunkName指定懒加载js的文件名称
  • import(‘/ * webpackPrefetch: true * /‘) 预加载
1
2
3
4
5
6
7
8
9
10
11
12
13
(function(){
var template = `<div><input><button id="btn">获取input值</button></div>`;
document.body.innerHTML+=template;

//点击获取input
const btn = document.querySelector("#btn");
btn.onclick= function(){
import(/*webpackChunkName: "jqueryA"*/'jquery').then(({default:$})=>{
console.log($);
console.log("input的值是",$("input")[0].value)
})
}
})();

预加载与预获取

  • 预获取

    1
    import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
  • 预加载

    1
    import(/* webpackPreload: true */ 'ChartingLibrary');

    测试中发现预加载和普通的import引入就没有多大的区别 了

  • 两者区别

    • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
    • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
    • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
    • 浏览器支持程度不同。

resolve

  • alias 创建 importrequire 的别名,来确保模块引入变得更简单

    1
    2
    3
    4
    5
    6
    resolve: {
    alias: {
    Utilities: path.resolve(__dirname, 'src/utilities/'),
    Templates: path.resolve(__dirname, 'src/templates/'),
    },
    },
  • extensions尝试按顺序解析这些后缀名。如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀。

    1
    2
    3
    resolve: {
    extensions: ['.js', '.json', '.wasm'],
    },
  • mainFiles 解析目录时要使用的文件名。

    1
    2
    3
    resolve: {
    mainFiles: ['index','main'],
    },

    参考链接:https://webpack.docschina.org/configuration/resolve/#resolvemainfiles

Loader

  • asset 自动选择base64图还是文件
  • asset/source 文件输出
  • asset/inline base64图片输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
module: {
rules: [{
test: /jpe?g|png|gif$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024,
},
}, //当是图片时,大小决定data url还是file输出
generator: {
filename: 'static/[hash][ext]', //匹配到该规则 时,输出的文件路径
},
}]
},
}
  • postcss-loader 为css打包时,添加样式前缀 参考链接:https://webpack.docschina.org/loaders/postcss-loader/

    • 方法1 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
    module.exports = {
    module: {
    rules: [
    {
    test: /\.css$/i,
    use: [
    'style-loader',
    'css-loader',
    {
    loader: 'postcss-loader',
    options: {
    postcssOptions: {
    plugins: [
    [
    'postcss-preset-env',
    {
    // 其他选项
    },
    ],
    ],
    },
    },
    },
    ],
    },
    ],
    },
    };

    postcss.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module.exports = {
    plugins: [
    [
    'postcss-preset-env',
    {
    // 其他选项
    },
    ],
    ],
    };

    Loader 将会自动搜索配置文件。

    webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module.exports = {
    module: {
    rules: [
    {
    test: /\.css$/i,
    use: ['style-loader', 'css-loader', 'postcss-loader'],
    },
    ],
    },
    };

    加大难度 ,为css添加前缀后,提出一个单独的css文件,并放置在css文件夹中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    module: {
    rules: [
    {
    test: /\.less$/,
    use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'less-loader',
    'postcss-loader'
    ],
    },
    ]
    },

Mode

  • development 开发模式
  • production 生产模式

production 模式下,会自动清理console.log

缓存

普通缓存

1
2
3
4
5
6
 output: {
- filename: 'bundle.js',
+ filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},

利用[contenthash]实现针对文件内容生成的文件名,当文件内容修改,hash会不同,也就解决浏览器缓存的问题,可以即时刷新。当文件内容未修改,contenthash值相同,浏览器会使用缓存文件

缓存第三方文件

将第三方库(library)(例如 lodashreact)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致。 这可以通过使用 SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin 插件的 cacheGroups 选项来实现。我们在 optimization.splitChunks 添加如下 cacheGroups 参数并构建:

1
2
3
4
5
6
7
8
9
10
11
12
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},

原文参考https://webpack.docschina.org/guides/caching/#output-filenames

打包CSS、js、img到各自文件总结

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
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry:'./app.js',
output:{
path: path.resolve(__dirname, 'dist'),
filename:'js/[name]-[contenthash].js',
clean:true
},
mode:'development',
module:{
rules:[
{test:/\.css$/,use:[MiniCssExtractPlugin.loader,'css-loader']},
{test:/\.(jpe?g|png)$/,type:'asset/resource',generator:{
filename:'img2/[name].[hash][ext]'
}},
{test:/\.html$/,use:'html-loader'} //如果html文件中存在图片,需要使用html-loader解析
]
},
resolve:{
alias:{
'@img':path.resolve(__dirname,'assets'),
'@css':path.resolve(__dirname,'css')
},
extensions:['.css','.js','.png','.jpeg','.jpg'],
mainFiles: ['index','1','global'],
},
plugins:[
new MiniCssExtractPlugin({
filename:'css/[name].css'
}),
new HtmlWebpackPlugin({
template:'./index.html',
title:'webpack html',
})
]
}
问题?使用htmlWebpackPlugin template时,使用到了图片,打包后图片不显示

devTool对比

是否可以追踪报错信息 备注
eval 构建:快速; 重建:最快,默认值 具有最高性能的开发构建的推荐选择。
source-map 具有高质量 SourceMap 的生产构建的推荐选择。打包会生成.map映射文件
inline-source-map 无ShourceMap文件,构建:最慢;重建:最慢。打包不会生成.map映射文件
none 生产环境用 具有最高性能的生产构建的推荐选择。 好像这个不行呢,使用报错

环境变量

1
2
"dev": "webpack server --config webpack.config.js --env development",
"prod":"webpack server --config webpack.config.js --env production",

webpack-merge合并webpack配置

  • webpack.config.js 入口配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const {merge}  = require('webpack-merge');
    const webpackComm = require('./webpack.config.comm');
    const webpackDev = require("./webpack.config.dev");
    const webpackProd = require('./webpack.config.production');
    module.exports =(e)=> {
    const mode = e.production?'production' : 'development';
    if(mode === 'production'){
    return merge(webpackComm,webpackProd,{mode})
    }else{
    return merge(webpackComm,webpackDev,{mode});
    }
    }
  • webpack.config.comm.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const {resolve} = require('path');
    module.exports = {
    entry:{
    app:'./app.js'
    },
    output:{
    path:resolve(__dirname,'dist'),
    filename:"js/[name].js",
    clean:true
    },


    }
  • webpack.config.dev.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
    const {resolve} = require("path");
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    module.exports = {
    module:{
    rules:[
    {
    test:/\.css$/,
    use:[MiniCssExtractPlugin.loader,'css-loader']
    },
    {
    test:/\.(jpe?g|png)$/,
    type:'asset/resource',
    generator:{
    filename:'[name]-[contenthash][ext]'
    }
    }
    ]
    },
    devtool:'eval',
    plugins:[
    new MiniCssExtractPlugin({
    filename:'[name].css'
    }),
    new HtmlWebpackPlugin({
    title:'webpack 打包的html'
    })
    ]
    }

  • webpack.config.prod.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
    const {resolve} = require("path");
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    module.exports = {
    module:{
    rules:[
    {
    test:/\.css$/,
    use:[MiniCssExtractPlugin.loader,'css-loader']
    },
    {
    test:/\.(jpe?g|png)$/,
    type:'asset/resource',
    generator:{
    filename:'img/[name]-[contenthash][ext]'
    }
    }
    ]
    },
    devtool:'eval',
    plugins:[
    new MiniCssExtractPlugin({
    filename:'css/[name].css'
    }),
    new HtmlWebpackPlugin({
    title:'webpack 打包的html'
    })
    ]
    }

    以上配置通过webpack.config.js入口配置文件,使用webpack-merge配置合并开发/生产 与公共配置合并成最后配置。

    此时在package.json中仅需要配置环境变量即可

    1
    2
    "build:prod": "webpack --config webpack.config.js --env production"
    "build:dev": "webpack --config webpack.config.js --env development"

devserver

几个重要参数
  • compress 为true 启用GZIP压缩

  • http2 使用 spdy 提供 HTTP/2 服务 HTTP/2 带有自签名证书:

  • https 默认情况下,开发服务器将通过 HTTP 提供服务。可以选择使用 HTTPS 提供服务

  • devMiddleware:{

    ​ writeToDisk: true,

    ​ } npx webpack server时,将内存中打包的代码写入到实体文件夹中

  • headers 为所有响应添加 headers:

    1
    2
    3
    4
    5
    devServer: {
    headers: {
    'X-Custom-Foo': 'bar',
    },
    },
  • hot 启用 webpack 的 热模块替换 特性:

  • liveReload 默认情况下,当监听到文件变化时 dev-server 将会重新加载或刷新页面。

  • open 告诉 dev-server 在服务器已经启动后打开浏览器。设置其为 true 以打开你的默认浏览器。

  • proxy 设置接口代理 假定项目中请求了以下接口

    1
    2
    http://www.shaoyuhong.cm/api/hello
    http://www.shaoyuhong.cm/axios/test
    • 最简单的一个代理 开发环境将请求地址为: http://www.shaoyuhong.cn/api/….

      1
      2
      3
      4
      5
      devserver:{
      proxy:{
      '/api':"http://www.shaoyuhong.cn"
      }
      }
    • 路径重写 开发环境将请求地址为: http://www.shaoyuhong.cn/api2/….

      1
      2
      3
      4
      5
      6
      7
      8
      devserver:{
      proxy:{
      '/api':{
      target:"http://www.shaoyuhong.cn",
      pathRewrite: { '^/api': '/api2' },
      }
      }
      }
    • 设置多个代理

      开发环境将请求地址为:http://127.0.0.1:18002/interface/…. && http://127.0.0.1:18002/shao/……

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      {
      context:['/interface'],
      target:'http://127.0.0.1:18002'
      },
      {
      context:['/api'],
      target:'http://127.0.0.1:18002',
      pathRewrite:{
      '^/api':'/shao'
      }
      }
  • 默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器。 如果需要secure:false,可以这样修改配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = {
    //...
    devServer: {
    proxy: {
    '/api': {
    target: 'https://other-server.example.com',
    secure: false,
    },
    },
    },
    };
  • 默认情况下,代理时会保留主机头的来源,可以将 changeOrigin 设置为 true 以覆盖此行为。 在某些情况下,例如使用 name-based virtual hosted sites,它很有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = {
    //...
    devServer: {
    proxy: {
    '/api': {
    target: 'http://localhost:3000',
    changeOrigin: true,
    },
    },
    },
    };

    参考链接:https://webpack.docschina.org/configuration/dev-server/#devserverproxy

插件

TerserWebpackPlugin
1
npm install terser-webpack-plugin --save-dev

该插件使用 terser 来压缩 JavaScript。

1
2
3
4
5
6
7
8
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};

参考文档:https://webpack.docschina.org/plugins/terser-webpack-plugin/

HtmlWebpackPlugin
1
npm install --save-dev html-webpack-plugin

HtmlWebpackPlugin 简化了 HTML 文件的创建,以便为你的 webpack 包提供服务

1
2
3
4
5
6
7
8
{
plugins: [
new HtmlWebpackPlugin({
template:'../index.html',
inject:true,//true,body,head 为true时,会在head中引入,并添加defer
chunk:['home','about'],//当前页面只展示某个chunk
})],
}

生成多个html文件 ??

1
2
3
4
5
6
7
8
9
10
const arr = ['shao','yu','hong'];
const htmls = arr.map(item=>{
return new HtmlWebpackPlugin({
template:'../index.html',
inject:'body',
filename:item+'.chunk.html',
chunk:[item],//当前页面只展示某个chunk
})
})
plugins: [...htmls],

内置变量

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
</body>
</html>
mini-css-extract-plugin

用于将style-loader生成的style标签样式,打包成css文件,使用link引入

外部扩展

防止将某些 包,如jQuery/Echarts…不打包在boundle中,而是使用script标签外部引入

例如,从 CDN 引入 jQuery,而不是把它打包:

index.html

1
2
3
4
5
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"
></script>

webpack.config.js

1
2
3
4
5
6
7
module.exports = {
//...
externals: {
jquery: 'jQuery', //jQuery是这个插件在window上挂载的什么就写什么 1.8.x的写法
jquery: '$', //3.x的写法
},
};

这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

1
2
3
import $ from 'jquery'; //jquery来自于externals配置的属性名

$('.my-element').animate(/* ... */);

优化

离线运行

渐进式网络应用程序(progressive web application - PWA),是一种可以提供类似于 native app(原生应用程序) 体验的 web app(网络应用程序)。PWA 可以用来做很多事。其中最重要的是,在**离线(offline)**时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的

参考链接:https://webpack.docschina.org/guides/progressive-web-application/

添加 Workbox

我们通过搭建一个拥有更多基础特性的 server 来测试下这种离线体验。这里使用 http-server package:npm install http-server --save-dev。还要修改 package.jsonscripts 部分,来添加一个 start script: 这一步其实可以不用做,添加workbox-webpack-pulgin后,使用webpack直接起服务效果也是相同的

添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件:

1
npm install workbox-webpack-plugin --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
24
25
26
27
  const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Progressive Web Application',
}),
+ new WorkboxPlugin.GenerateSW({
+ // 这些选项帮助快速启用 ServiceWorkers
+ // 不允许遗留任何“旧的” ServiceWorkers
+ clientsClaim: true,
+ skipWaiting: true,
+ }),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};

完成这些设置,再次执行 npm run build,看下会发生什么:

1
2
3
4
5
6
7
8
...
Asset Size Chunks Chunk Names
app.bundle.js 545 kB 0, 1 [emitted] [big] app
print.bundle.js 2.74 kB 1 [emitted] print
index.html 254 bytes [emitted]
precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js 268 bytes [emitted]
service-worker.js 1 kB [emitted]
...

现在你可以看到,生成了两个额外的文件:service-worker.js 和名称冗长的 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.jsservice-worker.js 是 Service Worker 文件,precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.jsservice-worker.js 引用的文件,所以它也可以运行。你本地生成的文件可能会有所不同;但是应该会有一个 service-worker.js 文件。

所以,值得高兴的是,我们现在已经创建出一个 Service Worker。接下来该做什么?

注册 Service Worker

接下来我们注册 Service Worker,使其出场并开始表演。通过添加以下注册代码来完成此操作:

index.js

1
2
3
4
5
6
7
8
9
10
11
12
  import _ from 'lodash';
import printMe from './print.js';

+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/service-worker.js').then(registration => {
+ console.log('SW registered: ', registration);
+ }).catch(registrationError => {
+ console.log('SW registration failed: ', registrationError);
+ });
+ });
+ }

再次运行 npm run build 来构建包含注册代码版本的应用程序。然后用 npm start 启动服务。访问 http://localhost:8080 并查看 console 控制台。在那里你应该看到:

1
SW registered

现在来进行测试。停止 server 并刷新页面。如果浏览器能够支持 Service Worker,应该可以看到你的应用程序还在正常运行。然而,server 已经停止 serve 整个 dist 文件夹,此刻是 Service Worker 在进行 serve。

Tree Shaking

  • 生成环境 usedExports

    usedExports 会将写的模块导入未使用的模块打包时丢弃

    1
    2
    3
    4
    mode:'production', 
    optimization: {
    usedExports: true,
    },

    1
    2
    3
    4
    5
    6
    import {    
    sum,
    cheng,
    sinData
    } from './until';
    console.log(sum(100,200));

    打包后只会生成

    1
    (()=>{"use strict";console.log(300)})();
  • sideEffects 将文件标记为 side-effect-free(无副作用)

    即,把某个文件标记为无副作用后,使用import导入的模块如果没有在代码中使用时,直接删除,包括CSS

    pagage.json中添加sideEffects:false

    1
    2
    3
    4
    5
    6
    {
    "name": "awesome npm module",
    "version": "1.0.0",
    "sideEffects": false, //false=>模块中任何文件无副作用,如果没有用到,可以随意删除,包括引入的CSS ;true=>有副作用,不可以随意删除
    "sideEffects": ["./src/some-side-effectful-file.js","./style.less","*.css"] //这个文件有副作用,涉及到这几处文件不能删除
    }
    • sideEffects === false时

      1
      2
      3
      4
      5
      6
      7
      import './style.less'
      import {
      sum,
      cheng,
      sinData
      } from './until';
      console.log(sum(100,200));

      打包后

      1
      (()=>{"use strict";console.log(300)})();
    • sideEffects === true时

      1
      (()=>{"use strict";var e,n,t,r,o,a,s,i,c,u,l,p,d,f,v={156:(e,n,t)=>{t.d(n,{Z:()=>i});var r=t(81),o=t.n(r),a=t(645),s=t.n(a)()(o());s.push([e.id,"ul,\nli {\n  list-style-type: none;\n}\nul {\n  display: flex;\n}\nul li {\n  flex: 1;\n  line-height: 2em;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n",""]);const i=s},645:e=>{e.exports=function(e){var n=[];return n.toString=function(){return this.map((function(n){var t="",r=void 0!==n[5];return n[4]&&(t+="@supports (".concat(n[4],") {")),n[2]&&(t+="@media ".concat(n[2]," {")),r&&(t+="@layer".concat(n[5].length>0?" ".concat(n[5]):""," {")),t+=e(n),r&&(t+="}"),n[2]&&(t+="}"),n[4]&&(t+="}"),t})).join("")},n.i=function(e,t,r,o,a){"string"==typeof e&&(e=[[null,e,void 0]]);var s={};if(r)for(var i=0;i<this.length;i++){var c=this[i][0];null!=c&&(s[c]=!0)}for(var u=0;u<e.length;u++){var l=[].concat(e[u]);r&&s[l[0]]||(void 0!==a&&(void 0===l[5]||(l[1]="@layer".concat(l[5].length>0?" ".concat(l[5]):""," {").concat(l[1],"}")),l[5]=a),t&&(l[2]?(l[1]="@media ".concat(l[2]," {").concat(l[1],"}"),l[2]=t):l[2]=t),o&&(l[4]?(l[1]="@supports (".concat(l[4],") {").concat(l[1],"}"),l[4]=o):l[4]="".concat(o)),n.push(l))}},n}},81:e=>{e.exports=function(e){return e[1]}},379:e=>{var n=[];function t(e){for(var t=-1,r=0;r<n.length;r++)if(n[r].identifier===e){t=r;break}return t}function r(e,r){for(var a={},s=[],i=0;i<e.length;i++){var c=e[i],u=r.base?c[0]+r.base:c[0],l=a[u]||0,p="".concat(u," ").concat(l);a[u]=l+1;var d=t(p),f={css:c[1],media:c[2],sourceMap:c[3],supports:c[4],layer:c[5]};if(-1!==d)n[d].references++,n[d].updater(f);else{var v=o(f,r);r.byIndex=i,n.splice(i,0,{identifier:p,updater:v,references:1})}s.push(p)}return s}function o(e,n){var t=n.domAPI(n);return t.update(e),function(n){if(n){if(n.css===e.css&&n.media===e.media&&n.sourceMap===e.sourceMap&&n.supports===e.supports&&n.layer===e.layer)return;t.update(e=n)}else t.remove()}}e.exports=function(e,o){var a=r(e=e||[],o=o||{});return function(e){e=e||[];for(var s=0;s<a.length;s++){var i=t(a[s]);n[i].references--}for(var c=r(e,o),u=0;u<a.length;u++){var l=t(a[u]);0===n[l].references&&(n[l].updater(),n.splice(l,1))}a=c}}},569:e=>{var n={};e.exports=function(e,t){var r=function(e){if(void 0===n[e]){var t=document.querySelector(e);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}n[e]=t}return n[e]}(e);if(!r)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");r.appendChild(t)}},216:e=>{e.exports=function(e){var n=document.createElement("style");return e.setAttributes(n,e.attributes),e.insert(n,e.options),n}},565:(e,n,t)=>{e.exports=function(e){var n=t.nc;n&&e.setAttribute("nonce",n)}},795:e=>{e.exports=function(e){var n=e.insertStyleElement(e);return{update:function(t){!function(e,n,t){var r="";t.supports&&(r+="@supports (".concat(t.supports,") {")),t.media&&(r+="@media ".concat(t.media," {"));var o=void 0!==t.layer;o&&(r+="@layer".concat(t.layer.length>0?" ".concat(t.layer):""," {")),r+=t.css,o&&(r+="}"),t.media&&(r+="}"),t.supports&&(r+="}");var a=t.sourceMap;a&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(a))))," */")),n.styleTagTransform(r,e,n.options)}(n,e,t)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(n)}}}},589:e=>{e.exports=function(e,n){if(n.styleSheet)n.styleSheet.cssText=e;else{for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(document.createTextNode(e))}}}},m={};function h(e){var n=m[e];if(void 0!==n)return n.exports;var t=m[e]={id:e,exports:{}};return v[e](t,t.exports,h),t.exports}h.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return h.d(n,{a:n}),n},h.d=(e,n)=>{for(var t in n)h.o(n,t)&&!h.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},h.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),h.nc=void 0,e=h(379),n=h.n(e),t=h(795),r=h.n(t),o=h(569),a=h.n(o),s=h(565),i=h.n(s),c=h(216),u=h.n(c),l=h(589),p=h.n(l),d=h(156),(f={}).styleTagTransform=p(),f.setAttributes=i(),f.insert=a().bind(null,"head"),f.domAPI=r(),f.insertStyleElement=u(),n()(d.Z,f),d.Z&&d.Z.locals&&d.Z.locals,console.log(300)})();

    参考链接: https://webpack.docschina.org/guides/tree-shaking/

Shimming 预置依赖

参考链接:https://webpack.docschina.org/guides/shimming/#granular-shimming

Shimming 预置全局变量

通过webpack new webpack.ProvidePlugin 暴露出全局变量,使得入口内部的js无论层级 都不需要再引入该模块,可以直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 const path = require('path');
+const webpack = require('webpack');

module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ plugins: [
+ new webpack.ProvidePlugin({
+ _: 'lodash',
$: 'jQuery'
+ }),
+ ],
};

如 a.js/b.js/c.js都可以使用使用$,无需再引入jquery

细粒度 Shimming

使用webpack后,this 不再指向window对象,代码中如果使用this对象访问window时,无法获取,this为undefined

为解决这个问题

你可以通过使用 imports-loader 覆盖 this 指向:

  • 安装 imports-loader

    1
    npm i imports-loader -D
  • 修改modules

    1
    2
    3
    4
    5
    6
    7
    8
    module: {
    + rules: [
    + {
    + test: require.resolve('./src/index.js'),
    + use: 'imports-loader?wrapper=window',
    + },
    + ],
    + },

    注意一点,使用imports-loader去修改this指针后,该js文件不能有其它类型的文件,如less/css等,否则将无法处理该文件

加载 Polyfills

安装

1
npm install --save-dev @babel/preset-env

webpack添加到module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.22"
}
]
]
}
}
},
全局 Exports
作用

用于非npm包时三方插件,这个插件没有使用es6或者comm.js语法导出,可以使用webpack导出并使用es6 comm.js导入的形式使用,请看下面的例子

假定我下载了一个三方插件,这个插件是普通的js文件,无export 或者 module.exports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//ani.js
class Ani{
constructor(status,success){
this.status = status;
this.success = success;
}
log(){
console.log(`提示信息,状态${this.status},成功数量:${this.success}`);
}
}
class CopyAni extends Ani{
constructor(status,success){
super(status,success)
}
}
const fileNanme = '这是ani.js'

问题来了,如何在我们的模块化js中 正常使用import /require导出fileName copyApi Ani这些函数、变量?

答案:**使用 exports-loader可以解决这个问题** https://webpack.docschina.org/loaders/exports-loader/

在webpack中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
test: require.resolve('./assets/ani.js'),
loader: 'exports-loader',
options: {
type: 'commonjs',
exports:
['fileNanme', 'multiple Ani AniFunction', 'multiple CopyAni CopyAniFunction','multiple 变量名/函数 导出的模块名'],
/*
最终生成的导出为
{
AniFunction: ƒ e(t,r)
CopyAniFunction: ƒ l(e,t)
fileNanme: "这是ani.js"
}
*/
},
},

现在我们在某个js文件中,可以正常导入使用

1
2
3
4
5
6
7
const methods = require('./ani');
const {AniFunction,CopyAniFunction,fileNanme} = methods;
const Ani1 = new AniFunction('成功AniFunction','100');
Ani1.log();
const Ani2 = new CopyAniFunction('失败CopyAniFunction','0');
Ani2.log();
console.log('导出的文件名:',fileNanme)

Web Workers

客户端原来和服务端可以相互通信,**原来以为,只能服务端 主动向客户端推送消息,并不能接收客户端数据**

客户端
1
2
3
4
5
6
7
8
9
10
11
12
const worker = new Worker(new URL('./server.js', import.meta.url));
const h4 = document.querySelector("#count");
setInterval(()=>{
console.log('我在问')
worker.postMessage({
question:
'你请求好了吗?',
});
},1000)
worker.onmessage = ({ data }) => {
h4.innerText = data;
};
服务端
1
2
3
4
5
6
7
8
9
10
var   i=0;
self.onmessage = ({ data: { question } }) => {
i++;
if(i<10){
self.postMessage(`Q:${question}----我还在请求中${i}`);
}else{
self.postMessage(`Q:${question}----请求成功!!!!!`);
}

};

web wokrder如果在webpack使用时,使用方式请参考:https://webpack.docschina.org/guides/web-workers/

打包图片资源

  • webpack5版本像url-loader,file-loder都是废弃不会直接使用的。我整了半天,虽然打包了图片,但是却打包了两次,并且无法显示图片了,
    如果想要使用这些废弃的旧功能,加上type: javascript/auto
  • esModule: false, 打包图片时需要关闭ES modules
  • outputPath:'static/img', 配置将资源文件输出的位置
1
2
3
4
5
6
7
8
9
10
11
12
module:{
rules:[{
test:/\.(png|jpe?g|gif)$/,
loader:'url-loader',
options:{
limit:30*1024,
esModule: false,
outputPath:'static/img',
},
type: 'javascript/auto'
}]
}

在webpack5中打包资源文件的修改

在 webpack 5 之前,通常使用:

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。

  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。

  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。

  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。url-loader与fileloader自动选择

    Rule.parser.dataUrlCondition:{maxSize:4*1024} 可以配置文件大小 的选择方式

当在 webpack 5 中使用旧的 assets loader(如 file-loader/url-loader/raw-loader 等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为 'javascript/auto' 来解决。

generator:filename可以支持一个路径,将文件分类打包到不同的文件夹中

exclude 除开某个条件外的所有情况,都按此方法处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
test:/\.(png|jpe?g|gif)$/,
type:'asset/resource',
generator:{
filename: 'static/img/[hash:10][ext]',
}
},
{
exclude:/\.(png|jpe?g|gif|html|js|ts|css|less)$/,
type:'asset/resource',
generator:{
filename: 'static/media/[hash:10][ext]',
}
}

原文参考:https://webpack.docschina.org/guides/asset-modules/

output.assetModuleFilename:’shao/[hash] [ext]’ 可以将静态资源统一设置一个路径,当有generator中filename指定的路径时,优先于generator

TypeScript

参考链接:https://webpack.docschina.org/guides/typescript/

使用typescript时,如果安装其它插件,如jquery/react,需要同时安装插件的类型申明文件,可点击链接自行安装

插件类型安装地址:https://www.typescriptlang.org/dt/search?search=

使用npx tsc --init 可以快速生成typescript的配置文件tsconfig.json

自定义插件

要在 Webpack 插件中实现替换打包文件中的 ‘A’ 为 ‘Z’,你可以使用 compiler.hooks.emit 钩子来修改生成的文件。

以下是一个示例,展示了如何使用自定义插件来替换打包文件中的 ‘A’ 为 ‘Z’:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制class ReplaceFilePlugin {
apply(compiler) {
compiler.hooks.emit.tap('ReplaceFilePlugin', (compilation) => {
for (const filename in compilation.assets) {
if (filename.endsWith('.js')) {
const asset = compilation.assets[filename];
const source = asset.source();
const replacedSource = source.replace(/A/g, 'Z');
compilation.assets[filename] = {
source: () => replacedSource,
size: () => replacedSource.length
};
}
}
});
}
}

module.exports = ReplaceFilePlugin;

在这个示例中,我们定义了一个名为 ReplaceFilePlugin 的自定义插件。在插件的 apply 方法中,我们注册了 compiler.hooks.emit 钩子,并在钩子函数中进行文件替换的逻辑。

我们遍历 compilation.assets 对象中的每个文件,判断文件名是否以 .js 结尾。如果是 JavaScript 文件,我们获取文件的源代码,并使用正则表达式将所有的 ‘A’ 替换为 ‘Z’。然后,我们将替换后的源代码重新赋值给 compilation.assets[filename],并更新文件的大小。

这样,在最终的打包结果中,所有 JavaScript 文件中的 ‘A’ 将被替换为 ‘Z’。

要使用这个自定义插件,你需要在 Webpack 配置文件中引入并实例化它:

1
2
3
4
5
6
7
8
javascript复制const ReplaceFilePlugin = require('./ReplaceFilePlugin');

module.exports = {
// ...其他配置项
plugins: [
new ReplaceFilePlugin()
]
};

webpack plugins执行顺序

Webpack 插件的钩子函数执行顺序是按照一定的生命周期顺序来执行的。以下是一些常见的 Webpack 插件钩子函数及其执行顺序的说明:

  1. beforeRun:在 Webpack 开始运行之前触发的钩子函数。
  2. run:Webpack 开始运行时触发的钩子函数。
  3. beforeCompile:在编译之前触发的钩子函数。
  4. compile:Webpack 开始编译时触发的钩子函数。
  5. compilation:每次成功完成一次编译时触发的钩子函数。
  6. make:在创建编译对象之前触发的钩子函数。
  7. afterCompile:在编译完成之后触发的钩子函数。
  8. emit:在生成资源到输出目录之前触发的钩子函数。
  9. afterEmit:在生成资源到输出目录之后触发的钩子函数。
  10. done:Webpack 完成构建后触发的钩子函数。

这些钩子函数按照从开始到结束的顺序依次执行,每个钩子函数都有特定的用途和时机。你可以根据需要选择合适的钩子函数来执行自定义的操作。

需要注意的是,有些钩子函数是同步执行的,而有些是异步执行的。对于异步执行的钩子函数,你需要在回调函数中显式地调用回调函数来通知 Webpack 继续执行。

在编写插件时,你可以根据具体的需求选择合适的钩子函数,并按照它们的执行顺序来编写相应的逻辑。

plugins tap与tapSync区别

在 Webpack 的自定义插件中,tapAsynctap 是两个不同的钩子函数注册方法,它们在使用方式和功能上有一些区别。

  1. tapAsynctapAsync 方法用于注册异步的钩子函数。这意味着你可以在回调函数中执行异步操作,并在操作完成后调用回调函数来通知 Webpack 继续执行。回调函数接受一个参数作为完成操作的标志,通常命名为 callback。你需要在回调函数中调用 callback 来告诉 Webpack 异步操作已完成。
1
2
3
4
5
javascript复制compiler.hooks.someHook.tapAsync('MyPlugin', (param1, param2, callback) => {
// 执行异步操作
// 在操作完成后调用 callback
callback();
});
  1. taptap 方法用于注册同步的钩子函数。这意味着你可以在回调函数中执行同步操作,而不需要显式地调用回调函数。回调函数接受参数,这些参数是由 Webpack 传递给钩子函数的。
1
2
3
javascript复制compiler.hooks.someHook.tap('MyPlugin', (param1, param2) => {
// 执行同步操作
});

总结来说,tapAsync 用于注册异步的钩子函数,需要显式地调用回调函数来通知 Webpack 异步操作已完成。而 tap 用于注册同步的钩子函数,不需要显式地调用回调函数。

在使用这两个方法时,你需要根据具体的需求和钩子函数的特性来选择合适的方法。

vite实现提出项目关键字

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import fs from 'fs';
import path from 'path';
import xlsx from 'xlsx';
import Md5 from 'blueimp-md5';
import os from 'node:os';
import en from './src/lang/en'


let recordI18nTextList = [];
// (I18n)("....")
// (I18n)('....')
// (I18n)("....",{})
const reg = /(?<=\(I18n\)\([\"\']).+(?=[\"\'][\)\,])/g;
const baseConfig = {
writeDir: path.resolve(__dirname, './src/lang'), // 需要写入的文件夹
};


// 加密函数
const hashString = (str) => {
return Md5(str);
}

// 生成待翻译 的excel
const createLangLibExcel = () => {
const json = recordI18nTextList.map(item => {
const key = `i18n_${hashString(item).substring(0, 8)}`;
return {
"字段名": key,
"中文": item,
"英文": en[key] || ''
}
});
// 创建工作簿 */
const wb = xlsx.utils.book_new();

// 创建工作表
const ws = xlsx.utils.json_to_sheet([
...json
]);
ws["!cols"] = [{ wch: 100 }, { wch: 100 }]; //设置列宽
// 将工作表添加到工作簿中
xlsx.utils.book_append_sheet(wb, ws, 'Sheet1');

// 将工作簿写入到本地文件系统中
const buffer = xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
fs.writeFileSync(path.resolve(__dirname, './国际化导出.xlsx'), buffer);

}

const writeLangLibToJs = () => {
let template = `export default {${os.EOL}****}`;
let zhParams = '', enParams = '';
for (let i = 0, length = recordI18nTextList.length; i < length; i++) {
const text = recordI18nTextList[i].replace(/[\"\']/g, "\\\"");
const key = `i18n_${hashString(recordI18nTextList[i]).substring(0, 8)}`;
const enText = en[key] ? `\'${en[key]}\'` : ('\'\'');
zhParams += ` ${key}:\'${text}\',${os.EOL}`;
enParams += ` ${key}:${enText},${os.EOL}`;
}

const zhContent = template.replace('****', zhParams);
const enContent = template.replace('****', enParams);
fs.writeFileSync(path.resolve(baseConfig.writeDir, 'zh.js'), zhContent, 'utf-8')

fs.writeFileSync(path.resolve(baseConfig.writeDir, 'en.js'), enContent, 'utf-8')
}


const editBuildJsOriginCode = (bundle)=>{
console.log('bundle:',bundle);
for (let fileName in bundle) {
if (fileName.endsWith('.js')) {
const fileData = bundle[fileName];

if (fileData.code) {
recordI18nTextList.forEach(text => {
const replaceReg = new RegExp(`(?<=\\([\"\'])${text}`, 'g')
fileData.code = fileData.code.replace(replaceReg, str => `i18n_${hashString(str).substring(0, 8)}`)
})

}
}
}
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
{
name: 'getI18n',
// 打包完成,还未输出到目录前
generateBundle(option, bundle) {

// 生成待翻译的excel
createLangLibExcel();

// 生成语言包
writeLangLibToJs();

// 生成的bundle修改内容
editBuildJsOriginCode(bundle);
},
// 每次所依赖的文件都会调一次
transform(fileContent, fileName) {
if (fileName.endsWith('vue') && !fileName.includes('node_modules')) {
const result = fileContent.match(reg);
recordI18nTextList = [...new Set([...recordI18nTextList, ...(result || [])])];
}
}
}
],
build: {
rollupOptions: {
output: {
entryFileNames: 'js/[name].js',
chunkFileNames: 'js/[name].js',
assetFileNames: (info) => {
const ext = info.name.substring(info.name.lastIndexOf('.') + 1,);
if (['jpeg', 'svg', 'png', 'gif', 'jpg'].includes(ext.toLowerCase())) {
return 'image/[name].[ext]'
}
if ('css' === ext) {
return 'style/[name].[ext]'
}
return 'otherAsset/[name].js'
},
},
},
},
})

其它事项

  • html-loader与html-webpack-plugin 有变量解析时,如

    1
    <title><%= htmlWebpackPlugin.options.title %></title>

    当配置了module:{loader:’html-loader’ }存在冲突,标题不能解析出来

  • ProvidePlugin 自动加载模块,而不必到处 import 或 require 。

    1
    2
    3
    4
    new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery'
    });
  • DefinePlugin DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和生产模式的构建允许不同的行为非常有用

    1
    2
    3
    4
    5
    6
    7
    8
    new webpack.DefinePlugin({
    PRODUCTION: JSON.stringify(true),
    VERSION: JSON.stringify('5fa3b9'),
    BROWSER_SUPPORTS_HTML5: true,
    TWO: '1+1',
    'typeof window': JSON.stringify('object'),
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    });

    值得注意的是,这里只能传【数字、字符串】。Array/object/booble/string需要使用json.stringify,属性名可以不用大写

webpack至此完结

2022/5/25

  • 本文标题:webpack学习计划来啦
  • 本文作者:邵预鸿
  • 创建时间:2022-01-10 18:11:09
  • 本文链接:/images/logo.jpg2022/01/10/webpack学习计划来啦/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!