webpack5学习 --- 代码分离(补充)

597 阅读6分钟

代码懒加载

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

动态import使用最多的一个场景是懒加载(比如路由懒加载)

例如: 我们点击某一个按钮后,希望再在页面上渲染某些内容的时候,可以使用懒加载

element.js

const divElem = document.createElement('div')
divElem.innerHTML = 'hello world'
export default divElem

index.js

const btn = document.createElement('button')
btn.innerHTML = 'click me'
document.body.appendChild(btn)

btn.addEventListener('click', () => {
  // 懒加载的简单实现
  import('./element').then(({default: element}) => document.body.appendChild(element))
})

Prefetch和Preload

之前的懒加载案例有一个可以优化的点在于:

默认情况下,是我们点击了按钮后,浏览器先去下载脚本,随后再去加载解析对应的脚本

此时如果脚本比较庞大的时候,可能给用户的会不是很好,

所以此时我们完全可以预先在合适的时机,将这些脚本先下载下来

之后在执行的时候,只要去加载和解析对应脚本即可

从webpack v4.6.0+开始, webpack增加了对预获取和预加载的支持

配置说明
prefetch(预获取)等待浏览器需要加载的资源加载完毕,浏览器空闲的时候,去将我们对应的资源预下载下来
一般用于显示将来某些导航下可能需要的资源
preload(预加载)和父chunk(也就是当前import所在的文件)一起被并行下载下来
一般用于显示当前导航下可能需要资源
btn.addEventListener('click', () => {
  // 开启preload和prefetch功能的方式是魔法注释,可以同时配置多个魔法注释
  // 但是一般 preload和prefetch设置一个值为true即可
  import(
    /* webpackPrefetch: true */
    /* webpackChunkName: 'element' */
    './element'
  ).then(({default: element}) => document.body.appendChild(element))
})

runtimeChunk

配置runtime相关的代码是否抽取到一个单独的chunk中:

runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码

也就是那些不是我们编写的,但是是运行时候必要的,由webpack为我们添加上的代码

这个时候,我们可以将这些runtime的代码进行抽离

这样当我们修改了业务代码的时候,这些runtime代码就不需要被再次加载了

说明
缺省值runtime不进行单独的抽取操作
multiple针对每个入口打包一个runtime文件,对于每个entry会生成runtime~${entrypoint.name}的文件
single打包一个runtime文件
true在单入口项目中等价于single,在多入口项目中等价于multiple
对象name属性决定runtimeChunk的名称
optimization: {
  runtimeChunk: {
    // 最后打包出来的文件会使用runtimeChunk.name和output.filename进行拼接,例如这里就是runtime.bundle.js
    name: 'runtime'
  }
}
optimization: {
  runtimeChunk: {
    // entryPoint是一个对象
    // 这个对象中有一个属性name,值是entry中的入口文件名
    name: (entryPoint) => {
      return `runtime~${entryPoint.name}`
    }
  }
}

原则上,不推荐将runtime拆分为多个包进行解析,因为这些js文件最后会按照一定的顺序在index.html中进行引入,

也就是说runtime中暴露的对象会被设置为全局对象,此时如果拆分为多个runtime,会存在全局变量污染的问题,

即后一个runtime中的同名对象将前一个runtime中的同名对象给覆盖了,如每一个runtime中都可能存在__webpack__require__对象

CDN

CDN称之为内容分发网络(Content Delivery Network或Content Distribution Network,缩写:CDN)

  • 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器
  • 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户
  • 来提供高性能、可扩展性及低成本的网络内容传递给用户

用户在请求资源的时候,会去离用户最近的那个服务器(边缘节点)去请求资源

如果资源存在,直接下载,如果资源不存在,去边缘节点的父节点查找,

依次类推,找到直接返回,没有找到继续去节点的父节点去查找,直到查找到源站(源节点)

在资源下发的时候,每一个节点服务器都会在该节点上将对应资源进行缓存

这样当下一个用户需要访问同样的资源的时候,就可以通过离用户最近的那个节点将用户所需资源进行返回。

在开发中,我们使用CDN主要是两种

  1. 打包的所有静态资源,放到CDN服务器,用户所有资源都是通过CDN服务器加载的
  2. 自己的业务代码存放在自己的服务器上,而将一些第三方资源放到CDN服务器上

全部放置到CDN服务器上

如果所有的静态资源都想要放到CDN服务器上,我们需要购买自己的CDN服务器

目前阿里、腾讯、亚马逊、Google等都可以购买CDN服务器

output: {
  path: path.resolve(__dirname, '../dist'),
  filename: '[name].bundle.js',
  // 将cdn路径设置为publicPath即可,所有的资源在打包的时候都会自动加上这个路径
  publicPath: 'https://www.example.com/cdn'
},

仅第三方放置到cdn服务器上

通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的、免费的CDN服务器上

国际上使用比较多的是unpkg、JSDelivr、cdnjs

国内也有一个比较好用的CDN是bootcdn

webpack.config.js

module.exports = {
  // externals 是webpack的顶层配置选项
  externals: {
    // key ===> 模块名
    // value ===> 模块所暴露的对象
    'dayjs': 'dayjs',
    'lodash': '_'
  }
}

index.html

<body>
  <!-- 因为所有的第三方库并没有打包到源代码中,所以需要在HTML中预先设置所有第三方库的CDN链接 -->
  <script src="https://unpkg.com/dayjs@1.8.21/dayjs.min.js"></script>
  <script src="https://unpkg.com/lodash@4.17.21/lodash.js"></script>
</body>

在开发的时候,我们服务器和浏览器都在本地,所以不需要将第三方包设置到cdn中,因为这些反而多次一举

此时我们可以使用ejs来对我们的html模板进行开发环境的判断

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Webpack</title>
</head>
<body>
  <!-- 在解析盖模板的时候 process.env.NODE_ENV已经被DefinePlugin设置了对应的值 -->
  <% if (process.env.NODE_ENV === 'production'){ %>
  <script src="https://unpkg.com/dayjs@1.8.21/dayjs.min.js"></script>
  <script src="https://unpkg.com/lodash@4.17.21/lodash.js"></script>
  <% } %>
</body>
</html>

css代码抽离

MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中

这个插件主要是在生产环境使用,开发环境可以选用

# 安装
npm install mini-css-extract-plugin -D

配置

config.common.js

const miniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = env =>  {
  const commonConfig = env => ({
        module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              // 开发环境使用style-loader,生产环境使用miniCssExtractPlugin
              env.production ? miniCssExtractPlugin.loader : 'style-loader',
              'css-loader'
            ]
          }
        ]
      }
  })
	
  return env.production ? merge(commonConfig(env), prodConfig) : merge(commonConfig(env), devConfig)
}

config.prod.js

const miniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [
    new miniCssExtractPlugin({
      // 因为是样式抽离,所以这里没有名为ext的placeholder
      filename: 'css/[name].[hash:6].css'
    })
  ]
}

env中的value

tips: env是node中一个非常特殊的对象,env对象中的所有的value值都会被转换为字符串后再进行存储

这是node对env对象的特殊处理,在node的其它对象中并不会存在这个情况

所以在webpack.config.js中如果需要在process.env中存取值的时候需要特别注意并进行相应的处理

const un = undefined
const num = 123

process.env.un = un
process.env.num = num

console.log(process.env.un, typeof process.env.un) // => undefined string
console.log(process.env.num, typeof process.env.num) // => 123 string

webpack中的hash值

hash

hash值的生成和整个项目有关系

比如我们现在有两个入口index.js和main.js

它们分别会输出到不同的bundle文件中,并且在文件名称中我们有使用hash

此时输出的bundle文件中的hash值是一致的

这个时候,如果修改了index.js文件中的内容,那么hash会发生变化

那就意味着两个文件的名称都会发生变化,即使main.js中的文件内容没有发生任何的改变

INVAlu.png

chunkhash

chunkhash会根据不同的入口来生成对应的hash值

此时有js文件index.jsmain.js,且index.js中引入了index.css

此时使用chunkhash构建后,文件名如下

INVch4.png

contenthash

contenthash表示生成的文件hash名称,只和内容有关系

不同内容的文件对应的contenthash的值都是不同的

同样此时有js文件index.jsmain.js,且index.js中引入了index.css

使用contenthash构建后,文件名如下

INVtcT.png

所以在项目中,如果使用到了hash值,推荐使用contenthash