前端模块化-webpack

231 阅读27分钟

模块打包工具

需要解决的问题

1.新特新代码的编译
2.模块化JS打包
3.支持不同类型的资源模块

webpack模块打包器(Module bundler)在打包过程当中通过模块加载器(Loader)对其进行编译转换,webpack还具备代码拆分(Code Splitting)的能力,将应用当中的代码,按照使用者的需要进行打包,我们就不用担心代码都打包到一起,产生的文件太大的问题

我们可以把页面初次加载所必须的文件打包到一起,其他模块的文件单独存放,等到需要用到的时候,进行异步加载这些模块,从而实现增量加载或者说渐进式加载,这样的话,就不用担心文件太碎或者文件太大的极端问题

前端模块类型的问题,支持在js中以模块化的方式,载入任意类型的文件

所有的打包工具,都是以模块化为目标,这里的模块化,是对于整个前端项目的模块化,比我们所说的js模块化更为宏观一些,让我们在开发阶段享受模块的优势,同时不必担心模块化对于生产的影响

webpack

webpack的快速上手

  1. npm init -y初始化一个package.json
  2. npm i webpack webpack-cli --dev安装完成之后,在package.jsonscripts中添加"build":"webpack",这样我们就可以通过npm run build --version查看我们所下载的webpack 版本,需要注意的是webpack4和5的版本差异还是很大的,这里主要说明的是4.x版本
  3. 这里可以使用serve工具来启动一个前端静态服务,查看打包好的项目,然后我们npm run build,webpack4以后的版本支持零配置的方式,直接启动打包,默认入口就是src/index.js出口是根目录下的dist/main.js

很多时候,我们需要去自定义入口等配置,这个时候需要去为webpack添加专门的配置文件,具体的做法就是在项目的根目录下添加一个webpack.config.js的文件,这个文件时一个运行在nodejs环境中的js 文件,所以我们需要按照commonjs方式去编写代码

const path = require('path')
/**
 * @type {import('webpack').Configuration}
 */
module.exports = {   // 这个文件可以导出一个对象,通过属性可以配置webpack选项
    entry: './src/main.js',  //因为是相对路径,所以前面的`./`是不能省略的
    output: {   // 设置输出文件的位置,值是一个对象
        filename: 'bundle.js', //filename可以指定输出文件的名称,
        path: path.join(__dirname,'output')  //指定输出文件所在的目录,path属性的值必须是一个绝对路径,可以通过node的path模块,通过前面的方法得到一个output的完整的绝对路径
    }
}

webpack4新增了一个工作模式的用法,这种用法大大简化了webpack的配置的复杂程度,当我们在之前的打包过程当中,命令行终端当中会打印一个配置的警告,如下图

大致的意思就是没有配置一个mode属性,mode有三种可配置属性,

  1. 当没有配置一个mode属性,这个时候webpack会默认使用production的模式去工作,这个模式下webpack会默认启动一些优化插件,例如代码压缩等,
  2. development开发模式,会自动优化打包速度,会添加一些调试过成功的一些辅助到代码当中,命令行启动方法是npm run build --mode develpoment,也可以直接配置到package.jsonscript
  3. none模式,运行最原始状态的打包,不会去做任何额外的处理

三种模式的具体的差异我们可以到官方文档中找到

除了通过cli参数指定我们的工作模式,我们也可以在webpack的配置文件中,配置我们的工作模式,这样的话,webpack就会按照我们配置的工作模式去执行任务了

module.exports = {   // 这个文件可以导出一个对象,通过属性可以配置webpack选项
    mode:'development', // 工作模式,有`development`/`none`/`production`三种
    entry: './src/main.js',  //因为是相对路径,所以前面的`./`是不能省略的
    output: {   // 设置输出文件的位置,值是一个对象
        filename: 'bundle.js', //filename可以指定输出文件的名称,
        path: path.join(__dirname,'output')  //指定输出文件所在的目录,path属性的值必须是一个绝对路径,可以通过node的path模块,通过前面的方法得到一个output的完整的绝对路径
    }
}

webpack打包结果运行原理

打包之后整体生成代码是一个立即执行函数,是webpack的工作入口,接收一个modules的参数,调用的时候,传入了一个数组,数组中的每一个元素都是一个参数列表相同的函数,每个函数对应的是源代码中的一个模块,每个模块都被包裹到这样的一个函数中,从而实现模块的私有作用域

资源模块的加载

webpack内部默认只会处理JS文件,当需要处理其他类型的文件时,需要各种不同的loaderloaderwebpack整个资源加载的核心,通过不同类型的loader就可以加载任何类型的资源

webpack的打包入口可以是各种类型的文件,但是一般我们还是把js文件作为打包入口,因为webpack的打包入口从某种程度上可以算是我们的应用的运行入口,就目前而言前端业务是由js驱动的,

传统的代码我们需要把css和js分离开,单独维护,单独引入,webpack建议我们在js中载入css,这到底是为什么呢

webpack不仅建议我们在js中引入css,而且建议我们在编写代码过程中引入任何当前代码所需要的资源文件,真正需要资源的不是应用,而是代码,代码想要正常工作就必须要加载对应的资源,这就是webpack的哲学

通过js代码去引入资源文件或者说建立资源文件和js的依赖关系,有一个很明显的优势的,按需加载,所有的资源文件不会缺失都是必要的,js驱动了整个前端应用,建立了这种依赖关系的好处是

逻辑比较合理,js确实需要这些资源文件 确保资源文件不会缺失,都是必要的

Loader

文件资源加载器 file-loader

webpack在打包的时候,遇见图片文件等,根据我们配置文件中的配置匹配到对应的文件加载器,文件加载器遇见这样的文件,先是将我们导入的文件,拷贝到输出的目录,然后将拷贝的目录的输出路径作为当前模块的返回值返回,这样对于我们的应用来说,所需要的资源就被发布出来了,同时可以通过模块的导出成员,拿到这个资源的访问路径。

Url加载器 url-loader 通过data urls的形式来表示文件,Data URLs是一种特殊的url协议,它可以用来直接表示文件。
传统的url一般要求服务器上有一个对应的文件,我们通过请求这个地址,来得到这个文件。
而DataUrl是当前url可以直接表示文件内容的方式,这个url中的文本已经包含了文件的内容,使用这种url不会去发送任何类型的http请求

最佳实践

对于项目中的小文件使用url-loader把小文件转换成data url的形式,以减少请求次数 大文件使用传统的file-loader的形式,以单个文件的形式存放,提高加载速度

配置方式如下

常用loader分类

编译转换类 css-loader 文件操作类 file-loader 代码检查类 代码检查

webpack 处理es2015 webpack因为模块打包需要,所以处理importexport做一些相应转换,并不能处理其他的es6特性,如果需要处理这些新特性,我们需要配置一个额外的编译型loader,最常见的就是babel-loader

使用方法如下:
npm i babel-loader @babel/core @babel/preset-env 由于babel-loader需要依赖babel的核心模块@babel/core和用于去完成具体特性转换插件的集合@babel/preset-env

webpack默认只是一个打包工具,不会去处理代码中的es6或者更新版本的新特性,如果需要处理这些新特性,我们需要为这些js代码配置单独的加载器来实现

webpack加载资源的方式

js代码中的资源加载方式

  1. 遵循ESM标准的import声明
  2. 遵循CommonJS标准的require函数 如果需要通过require函数载入一个ESM的话,需要注意的是,对于ESM的默认导出,需要通过require('xxxx').default来获取
  3. 准寻AMD标准的define函数和require函数

webpack兼容多种模块化标准,除非必要情况,否则不要在项目中混合使用这些标准,混用的话可能会大大降低项目的可维护性

独立加载器加载资源

也会处理加载资源当中的一些导入的模块

css-loader加载css文件的时候,@import指令和部分属性中的url函数也会触发相应的资源模块加载
html-loader加载的html文件中图片标签的的src属性也会触发相应的资源模块加载

关于html-loader的配置问题

webpack核心工作原理
根据配置找到打包的入口,一般是一个js文件,然后根据入口文件的import和require之类的语句,解析这个文件依赖的资源模块,解析资源模块对应的依赖,最后解析出项目中所有文件之间依赖关系的依赖树,webpack可以递归这个依赖树,找到每个节点对应的资源文件,再根据modules/rules找到对应资源文件所需要的对应的loader,最后将加载的结果放到bundle.js中,从而实现整个项目的打包。

loader机制是webpack的核心

手写一个loader

webapck加载资源的过程类似于工作中的管道,最终结果必须是一段js代码
loader负责资源文件从输入到输出的转换,对于同一个资源可以依次使用多个loader

每个Loader都需要导出一个一个函数,整个函数就是loader对资源的处理过程,输入就是加载到的资源文件的内容,最终输出(return)的是一段js代码字符串

// 手写导出
const  marked =  require('marked')
module.exports = source => {
  console.log(source)
  const  html = marked(source)
  // return `module.exports = ${JSON.stringify(html)}`
  return `export default ${JSON.stringify(html)}`	// 两种导出方式都可以

}

webpack的插件

webpack的插件机制,增强webpack在项目自动化方面的能力,解决项目中除了资源加载以外的自动化工作

拷贝文件
压缩代码
清除dist目录 clean-webpack-plugin

自动生成使用打包结果的html html-webpack-plugin

上线时,我们需要同时去发布根目录的html文件和dist目录中的所有打包结果,还要确认html文件中的js文件的引用路径,基于这两个问题,使用html-webpack-plugin实现对html文件中的打包后的js文件的自动注入,不需要去硬编码 使用方式

  1. npm i html-webpack-plugin --dev
new HtmlWebpackPlugin({
    template:'index.html',	//  指定模板
    title: "webpack plugin sample",	 
    meta: {
        viewport:'width=device-width'
    }
})

这里的title设置需要在模板的html文件中的title标签里面添加模板语法<%= htmlWebpackPlugin.options.title %>并且这个title属性的设置在使用html-loader时,是不能生效的,这里需要注意一下。htmlWebpackPlugin.options这个属性可以访问到插件的配置数据,是插件内部提供的变量。

如果我们需要同时输出多个页面文件,可以在Plugins中加入多个HtmlWebpackPlugin的实例对象,每个对象负责生成一个页面文件的

plugins:[
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
        template:'index.html',
        title: "webpack plugin sample",
        meta: {
            viewport:'width=device-width'
        }
    }),
    new HtmlWebpackPlugin({
        template:'index.html',
        title: "webpack plugin",
        filename:'about.html'
    })
]

在项目中,我们还有一些不需要处理的文件,打包的时候只要直接复制到目标目录,copy-webpack-plugin,这个函数需要我们传入一个数组,用于指定需要拷贝的文件路径,可以是一个通配符,目录,文件的相对路径

 new CopyWebpackPlugin([
    // 'public/**',
    'public'        // 这里表示在打包的时候将public目录的内容全部拷贝到输出目录
])

手写一个webpack插件

webpack的插件机制就是我们软件开发过程中的常见的钩子机制,webpack工作过程中有很多环节,webpack在每个环节都埋下了钩子,在去开发插件的时候,在不同节点挂载不同的任务

wepack要求插件必须是一个函数或者是一个包含apply方法的对象,一般我们会把一个插件定义成一个类,在类中定义apply方法,在使用的时候,通过这个类构建一个实例来使用

插件是通过往webpack申明周期的钩子函数里去挂载任务函数来实现的,如果需要深入了解插件机制,我们需要深入了解webpack底层实现原理,需要通过阅读源码的方式来了解

源码

// 实现一个去除bundle.js文件中行开头注释的plugin
class myPluguin {
    apply(compiler) {  // 这个方法在webpack启动的时候被自动调用
     // compiler参数 是webpack工作过程中最核心的对象,包含了此次构建的所有的配置信息,我们也是通过这个对象去注册钩子函数
        // console.log('myPlugin 启动',compiler)   // 通过compiler.hooks属性继续点,可以访问到emit钩子,
        compiler.hooks.emit.tap('myPluguin',compilation => {     // 通过tap方法注册钩子函数,tap方法接受两个参数,一个是插件名称,第二个是需要挂载到这个钩子上的函数
            // compilation 可以理解为此次打包过程中的上下文,我们所有的打包过程中产生的结果都会放到这个对象中
            for(const name in compilation.assets) { // compilation.assets 获取即将写入文件中的资源文件信息
                // console.log('name',name);    // 文件名称
                // console.log('name1',compilation.assets[name].source()); // 文件内容需要通过值的source方法来获取

                if(name.endsWith('.js')){
                    const contents = compilation.assets[name].source()
                    const  withoutComments = contents.replace(/\/\*\*+\*\//g,'') // 替换掉打包结果js文件内容的注释部分
                    compilation.assets[name] = {
                        source: () => withoutComments,
                        size: () => withoutComments.length
                    }

                }
            }
        })  
    }
}

增强webpack开发体验

自动编译 watch工作模式

监听文件变化,自动编译打包 具体用法就是启动webpack命令的时候添加一个 --watch的参数就好了,这样的话,webpack就会以监视模式运行,在打包完成过后,不会立即退出,会等待文件变化,自动编译新的结果
"watch": "webpack --watch"我们需要在package.jsonscript添加这样的指令就可以使用文件监听的功能了

自动刷新浏览器的功能BrowserSync
原理:webpack自动打包代码到dist当中,dist文件的变化被browser-sync监听了,从而自动刷新 > 操作麻烦,效率降低(频繁的磁盘读写)

webpack-dev-server

提供了用于开发的HTTP server,并且将自动编译和自动刷新浏览器等一些列对于开发友好的功能进行了集成。  
1. `npm i webpack-dev-server --dev`,安装完成之后可以定义到`npm script`中,==>`"start": "webpack-dev-serve"`,运行`npm run start`启动服务,我们还可以加入`--open`参数,使之打包完成之后自动打开浏览器
2. `webpack-dev-server`内部会自动使用webpack去打包我们的应用,打包的结果**暂时被放在了内存中**,这样会大大减少不必要的io操作,大大提高我们的构建效率,并且启动一个`http server`去运行我们的打包结果,运行过后,还会监听代码变化,发生变化会自动打包
3. `webpack-dev-server`默认会将构建输出的文件作为构建服务的资源文件,只要通过webpack打包正常输出的文件都能正常被访问到,但是一些静态资源也需要作为开发服务器的资源被访问的话,需要额外的在`webpack`配置文件中添加额外的配置
```
devServer:{
    contentBase:['./public'] // 指定额外指定一个额外的资源路径的操作方式
},
```

日常开发中由于同源策略的问题,我们经常需要本地跨域访问接口地址,这里我们介绍的是配置代理服务,如下图

关于主机名是HTTP协议中的相关概念,有兴趣的可以去自行了解

Source Map

通过构建编译将源代码转换为生产环境中可以运行的代码,这种代码和源代码之间有很大的差异,这种情况下我们需要去调试我们的应用或者是产生错误信息无法定位,无论调试或者报错都是基于转换过后的运行代码实现的,Source Map就是用来解决这些问题而产生的,别名源代码地图,用来映射转换后的代码和源代码之间的关系

目前很多第三方库发布的文件都有.map文件,这样的文件实际上是一个json格式的文件,记录的是代码转换前后的映射关系,由于对生产没有意义,所以生产环境不需要这样的文件

version 当前文件使用的source map标准的版本
source 转换之前源文件的名称,由于可能是多个文件合并成一个文件,所以这是一个数组 names 源代码中使用的成员名称 mappings 是一个base64-vlq编码的字符串,记录着转换前后代码之前的映射关系

我们一般会在转换过后的代码中添加一行注释的方式引入这样的文件,引入方式//# sourceMappingURL=xxx.map

作用: 解决的是前端引用了构建编译过后,导致编写的代码和运行的代码不一样,产生的调试的问题

Source Map配置

当我们在webpack中使用devtool属性配置了source-map,生成的bundle.js文件中自动引入了我们之前所说的注释和生成了.map的文件

webpack目前为止支持12种不同的方式配置source-map,每种方式生成的source-map的效果和速度都是不一样的,效果好的速度慢,速度快的效果差

不同的配置方式的对比如下图

从初次构建速度,监视模式重新build速度,是否适合生产使用,构建后的质量四个维度进行对比

eval模式
这个模式下,会将我们每个模块所转换之后的代码都放在eval函数中执行,在字符串最后通过sourceURL的方式去说明文件所对应的url路径,只能定位文件,这种模式下不会生成source-map文件,这种模式构建速度是最快的

使用这样的webapck配置,我们可以自己去对比不同配置之间的差异

const HtmlWebpackPlugin = require('html-webpack-plugin')

const allModes = [
    'eval',
    'cheap-eval-source-map',
    'cheap-module-eval-source-map',
    'eval-source-map',
    'cheap-source-map',
    'cheap-module-source-map',
    'inline-cheap-source-map',
    'inline-cheap-module-source-map',
    'source-map',
    'inline-source-map',
    'hidden-source-map',
    'nosource-source-map',
]

/**
 * @type {import('webpack').Configuration}
 */

module.exports = allModes.map(item => {
    return {
        devtool: item,
        mode: 'none',
        entry: './src/main.js',
        output: {
            filename: `js/${item}.js`
        },
        module: {
            rules: [
                {
                    test:/\.js$/,
                    use: {
                        loader:'babel-loader',
                        options:{
                            presets: ['@babel/preset-env']
                        }
                    }
                }
            ]
        },
        plugins:[
            new HtmlWebpackPlugin({
                filename:`${item}.html`
            })
        ]
    }
})

总结:

  1. eval- 是否使用eval执行模块代码
  2. cheap - Source Map是否包含行信息 -- 加了cheap就不包含行信息
  3. module - 是否能够得到Loader处理之前的源代码 --添加module就可以得到处理之前的源代码
  4. nosource - 抛错时能看到错误出现的位置,但是看不到源代码,为了生产模式保护源代码
  5. hidden - 构建的时候生产了map文件,但是代码中并没有通过注释的方式引入,我们在开发工具中看不到效果,一旦出现了问题,再把map文件引入回来
  6. inline - source-map模式下,map文件以物理文件方式存在,而inline-source-mapbase64dataUrl的方式嵌入代码中,会使代码体积变得很大,eval-source-map 也是使用行内的方式把source-map嵌入进来的,一般来说是最不可能用到的,因为会使代码体积变得很大。
选择合适的source map

开发环境个人选择cheap-module-eval-source-map

  1. 代码定位到行就够了
  2. 一般使用框架开发,loader转换前后代码差异较大,调试一般都是要调试源代码
  3. 虽然首次打包慢,但是重新打包快,多数情况下都是以监视模式下重新打包 生产个人选择 none或者nosource-source-map
  4. 调试是开发阶段的事情
  5. 防止源代码暴露

选择没有绝地,适合自己的就是最好的

HMR 模块热更新

应用程序运行过程中实时的替换应用中的某个模块,应用的运行状态不受影响 热更新会只将修改的模块实时替换至应用中,不必完全刷新应用

HMR目前已经集成在了webpack-dev-server中,我们只需要在运行webpack-dev-server命令是添加参数--hot就可以了,或者在配置文件中添加配置

这里配置需要分为两步

  1. 当我们js文件修改有时候会刷新页面,是因为HMR并不是像其他模块一样开箱即用
  2. 而我们样式文件可以实时的热更新,是因为style-loader中自动处理了样式的热更新。
  3. 而我们使用框架修改js的时候,我们没有做任何处理,修改js文件也支持热更新,因为框架下的开发,每种文件都是有规律的,框架都在脚手架创建项目内部集成了HMR方案

总结:我们还是需要自己手动处理js模块热更新后的热替换,不同的模块有不同的逻辑,逻辑不同,处理的过程也是不同的

注意事项:

  1. 处理HMR的代码报错会导致自动刷新 找不到错误时,我们可以在 devServer: { hot: true }改成devServer: { hotOnly: true },这里的hotOnly只会使用HMR,不会回退(fallback)到刷新(live reloading)
  2. 没有启用HMR的情况下,HMR API报错,是因为我们再处理js热更新的时候使用到的module.hot对象是HMR提供的,当没开启HMR的时候自然不会有module.hot这个对象,解决办法在外层加一个判断
  3. 代码中很多无关处理热更新的代码,在打包的时候都会被移除掉

生产环境的优化 生产环境注重运行效率,开发环境注重开发效率,为不同的工作环境创建不同的配置

  1. 配置文件根据环境的不同导出不同的配置
  2. 一个环境对应一个配置文件,不同环境有不同的配置文件

大型项目不同环境对应不同配置文件,小型项目可以直接在一个配置文件里面判断, 这里需要提一点的就是对于通用配置和生产配置的合并,有一个webpack-merge 模块可以提供这样的功能

Tree Shaking

生产模式下会自动开启,自动检测出未引用代码,并移除 并不是webpack的一个配置选项,而是一组功能搭配使用后的优化效果,生产模式下自动启用

开发模式配置如下
需要在optimization属性中配置usedExportsminimize属性为true即可

我们还可以进一步优化

普通的打包结果是将我们的每一个模块最终放在一个单独的函数中,如果我们的模块很多,那么在输出结果中,我们会有很多模块函数
concatenateModules: true作用就是尽可能的将所有模块合并输出到一个函数中,既提升了运行效率,有减少了代码体积
这个特性又被称之为Scope Hoisting也就是作用域提升

关于Tree Shaking和babel Tree Shaking的实现的前提就是使用EMS去组织我们的代码,又因为webpack处理的代码必须使用ESM的方式来实现的模块化,而我们处理js新特性的时候,需要使用到babel-loaderpreset-env,这个插件转换的时候会把ESM的代码转换成commonjs的形式

最新版本的babel-loader并不会导致tree-shaking失效,因为自动关闭了ESM的转换插件,当当前环境支持EMS的时候,babel-loader会生成一个标识,preset-env会根据这个标识自动禁用ESM的转换代码

sideEffects 副作用

webpack4中新增的新特性,允许使用配置的方式标识我们的代码是否有副作用,为Tree Shaking提供更大的压缩控件

副作用:模块执行时除了导出成员之外所做的其他事情

sideEffects一般用于npm模块包标记是否有副作用

由于官网中介绍时把sideEffectsTree Shaking混到了一起,很多人以为两者有什么因果关系,事实上两者没有那么大的关系

// webpack.config.js中
optimization: {
  sideEffects: true,  // 开启功能 ,production模式下回自动开启   
}
// package.json
"sideEffects": false	// 标识当前的项目中的代码都没有副作用

开发环境中需要两个地方设置sideEffects,一个是开启功能,一个是标识当前项目代码没有副作用

optimization: { sideEffects: true }开启之后,webpack在打包的时候就会检查在当前代码所属的工程的package.json文件中是否有sideEffects的标识,来判断这个模块有没有副作用,如果这个模块没有副作用,没用到的模块就不会被打包

注意: 使用这个属性的前提就是确定自己的代码真的没有副作用,否则在webpack打包时就会误删掉那些有副作用的代码,当有副作用的代码需要保留的时候我们可以配置"sideEffects": true,或者

optimization: {
  sideEffects: [
    'xx/xx/xx.js', // 可以加入副作用文件路径,这样的话打包的时候,就会把这些有副作用的代码也加入打包的代码中   
    '*.css' // 也可以用路径通配符
  ]  
}

Code Splitting 代码分包/代码分割

所有的代码最终都会被打包到一起,bundle体积过大,大多数时候,在应用开始工作的时候,并不是所有模块都需要加载进来的,对于页面加载等都不太合理,合理的方案是把打包结果分到多个bundle中,分包,根据运行需要按需加载

http1.1 缺点

  1. 不能同时对同一个域名下发起很多次请求
  2. 每次请求都会有一定的延迟
  3. 请求header浪费贷款流量,所以模块打包是有必要的

分包的方式

  1. 多入口打包
  2. ESM动态导入功能,实现模块的按需加载
多入口打包

一般适用于传统的多页应用程序,常见的划分规则就是一个页面对应一个打包入口,不同页面中的公共部分,提取到公共的结果当中

关于多入口打包的设置如下图:

html-webpack-plugin插件默认输出一个自动注入所有打包结果的html文件,所以要想对应的html注入对应的打包结果的js文件我们需要指定输出的html所需要注入的bundle

提取公共模块 Split Chunks

不同的入口中肯定会有公共的模块,共同使用比较大的三方js库的时候,影响会比较大,这个时候就需要用到公共模块提取

开启公共模块的提取配置很简单

打包过后会生成公共的模块部分的bundle.js

webpack的动态导入

按需加载使我们开发浏览器应用非常常见的需求,需要某个模块的时候,加载某个模块,极大地节省带宽和流量 webpack中支持动态导入的方式实现模块的按需加载,而且所有动态导入的模块都会被自动提取到单独的bundle.js中,从而实现分包

相对于多入口的方式,动态导入更为灵活,通过代码的逻辑控制模块的加载和加载时机,分包的最终目的有一点就是让模块实现按需加载,从而提高应用的响应速度 上图就是vue项目中在配置路由的时候,通过路由映射组件,使用动态导入的方式实现按需加载。

动态导入会生成N多的bundle.js,当你需要给对应的文件命名的时候,图中的魔法注释/* webpackChunkName: "bindBankCard" */可以帮你,这样生成的bundle.js就会被命名为bindBankCard.bundle.js

相同的webpackChunkName最终会被打包到一起,从而实现我们所需要的按业务模块打包的需求

MiniCSSExtractPlugin 提取css到单个文件

是一个可以将css代码从打包结果中提取出来的插件,通过这个插件可以实现css的按需加载

使用MiniCSSExtractPlugin的话,我们的样式会单独存放到文件中,也就不再需要style-loader,取而代之的使用MiniCSSExtractPlugin.loader中提供的loader,从而实现样式文件通过link标签的方式注入

使用方式如下

个人经验就是css代码超过150kb的时候才需要单独抽出来引入

当我们把css单独抽离出来的时候,我们发现,webpack内置的压缩插件不会对js之外的文件进行压缩,其他的资源文件的压缩,需要额外的插件来支持

optimize-css-assets-webpack-plugin 进行css压缩
terser-webpack-pluginwebpack内部自带的js压缩插件

输出文件名hash

生产模式下,文件名使用hash hash分为三种,效果各不相同

  1. filename: '[name]-[hash].bundle.js,项目级别的,项目中任意一个地方发生了变化,所有文件的hash都会发生变化
  2. filename: '[name]-[chunkhash].bundle.jschunk级别的,在打包过程中,只要是同一路(chunk)的打包,chunkhash都是相同的,修改某个chunk的内容,只有对应的chunkhash发生变化,引入这个chunk的文件hash也会发生变化
  3. filename: '[name]-[contenthash].bundle.js,这是文件级别的hash,根据输出文件的内容生成的hash,不同的文件hash值不同,修改一个文件只有对应文件的chunk和引入这个chunk的文件的hash才会发生变化

相比较而言contenthash应该是解决缓存问题最好的方式了,因为它精确的定位到了文件级别的hash,只有对应的文件发生了变化,才会更新掉文件名,算是最适合解决文件缓存问题的了

hash值一般有20位,通常我们用不到这么长,一般都是用的8位,webpack允许指定hash的长度,通过:8冒号数字的形式指定hash的长度,举例:filename: '[name]-[contenthash:8].bundle.js

控制缓存的话,8位的contenthash应该是最通用的选择了