webpack5笔记

681 阅读16分钟

webpack是什么?

  • webpack是一个静态的模块化打包工具,为现代的JavaScript应用程序

webpack的依赖

webpack的运行是依赖node环境的

webpack的安装

  • webpack的安装目前分为两个:webpackwebpack-cli
  • npm install webpack webpack-cli (-g)
webpack和webpack-cli的关系
  • 执行webpack命令,会执行node_modules下的.bin目录下的webpack
  • webpack在执行时是依赖webpack-cli的,如果没有安装就会报错
  • 而webpack-cli中代码执行时,才是真正利用webpack进行编译和打包的过程
  • 所以在安装webpack时,我们需要同时安装webpack-cli(第三方的脚手架事实上是没有使用webpack-cli的,而是类似于自己的vue-service-cli的东西)

传统开发存在的问题

我们的代码存在什么问题呢?某些语法浏览器是不认识的
  • 使用了ES6的语法,比如const,箭头函数等语法
  • 使用了ES6的模块化语法
  • 使用了commonjs的模块化语法
  • 在通过script标签引入时,必须添加上type=‘module’属性

webpack默认打包

  • 我们可以通过webpack进行打包,运行打包之后的代码

    • 在目录下直接执行webpack命令 webpack
  • 生成一个dist文件夹,里面存放了一个main.js的文件,就是我们打包之后的文件:

    • 这个文件的代码被压缩和丑化了
    • 这个文件的代码依然存在ES6的语法
  • webpack是如何确定我们的入口呢?

    • 事实上,当我们运行webpack时,webpack会查找当前目录下src/index.js作为入口
    • 所以,如果当前项目中没有存在src/index.js文件,那么会报错

webpack配置文件

  • 我们可以在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件:
const path = require('path')
​
mudule.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist') // 这里的路径要是绝对路径
    }
}
  • 继续执行webpack命令,依然可以正常打包
webpack指定配置文件
  • webpack默认会去读取当前目录下webpack.config.js文件

  • 但是如果我们的配置文件并不是命名为webpack.config.js,而是其他名字呢?

    • 比如我们将webpack.config.js 修改成了wk.config.js

    • 这个时候我们可以通过 --config 来指定对应的配置文件

      webpack --config wk.config.js

  • 但是每次这样执行命令来对源码进行编译会非常繁琐,所以我们可以在package.json中添加一个新的脚本

    • { "scripts": { "buile": "webpack --config wk.config.js" } }

webpack的依赖图

  • webpack到底是如何对我们的项目进行打包的呢?

    • 事实上webpack在处理应用程序是,它会根据命令或者配置文件找到入口文件
    • 入口文件开始,会生成一个依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块
    • 然后遍历这个依赖关系图,打包一个一个模块

    css-loader的使用

  • 当我们的项目中有css文件时,用webpack对项目进行打包时,会报错

  • webpack的错误信息告诉我们需要一个loader来加载这个css文件

loader是什么?
  • loader可以用于对模块的源代码进行转换
  • 我们可以将css文件也看成一个模块,我们是通过import来加载这个模块
  • 在加载这个模块时,webpack其实并不知道如何对其进行加载,我们必须制定对应的loader来完成这个功能
css-loader的安装

npm install css-loader -D

css-loader的使用方案
  • 如何使用这个loader来加载css文件?有三种

    • 内联方式
    • CLI方式(webpack中不再使用)
    • 配置方式
  • 内联方式:内联方式使用较少,因为不方便管理

    • 在引入的样式前加上使用的loader,并且用 分割

      import 'css-loader!../css/style.css'
      
loader配置方式
module.exports = {
    // ... 省略
    module: {
        rules: [
            {
                test: /.css$/, // 设置要用loader的文件
                // loader: 'css-loader' 写法一,适用于只用一个loader,且没有配置选项
                // use: ['css-loader']  写法二:适用于使用多个loader,且没有配置选项
                use: [
                    { loader: 'css-loader' } // 写法三:适用于需要配置选项的loader
                ]
            }
        ]
    }
}
认识style-loader
  • 通过css-loader来加载css文件,但是会发现这个css在我们的代码中并没有生效(页面没有效果)
  • css-loader只是负责将.css文件进行解析,并不会将解析后的css插入页面
  • 如果我们希望再完成插入style的操作,那么我们还需要另一个loader,style-loader

npm install style-loader -D

配置style-loader
  • 注意loader的执行顺序是从右往左(或者说从上往下,从后往前) ,所以我们需要将style-loader写到css-loader的前面
module.exports = {
    // ... 省略
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',  
                    'css-loader'
                ]
            }
        ]
    }
}

处理less文件

  • 在开发中,可能会使用less,sass, stylus的预处理器来编写css样式,效率会更高
  • 首先需要确定,less,sass等编写的css需要通过工具转化成普通的css
less-loader处理less

处理less需要安装less,但是less-loader依赖了less工具,所以直接安装less-loader即可

npm install less-loader -D

module.exports = {
    // ... 省略
    module: {
        rules: [
            {
                test: /.less$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'less-loader'
                ]
            }
        ]
    }
}

浏览器兼容性

  • 开发中,浏览器的兼容性问题,我们该如何去解决和处理?

    • 这里的兼容性指不同的浏览器支持的特性,比如css特性,js语法,之间的兼容性
  • 在很多的脚手架配置中,都能看到类似于这样的配置信息

    • 这里的百分之一,就是指市场占用率

      > 1%  // 市场占有率超过 1%
      last 2 versions // 最后两个版本
      not dead // 在24个月内有进行维护
      

浏览器市场占有率

  • 我们可以在哪些可以查询到浏览器的市场占有率

    • 这个好用的网站,也就是我们工具通常会查询的一个网站就是caniuse
认识browserslist工具
  • 如何可以在css兼容性和js兼容性共享我们配置的兼容性条件

    • browserslist
  • browserslist是什么?

    • browserslist是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置
browserslist编写规则
  • defaults:browserslist的默认浏览器
  • not ie <= 8:排除前先查询选择的浏览器
  • 5% :通过全局使用情况统计信息选择的浏览器版本。
  • dead:24个月内没有官方支持或更新的浏览器。
命令行使用browserslist

npx browserslist ">1%, last 2 version, not dead"

配置browserslist

  • 我们可以通过两种方案配置browserslist

    • 方案一:在package.json文件中配置
    • 方案二:单独的一个配置文件 .browserslistrc文件
  • 方案一:package.json配置

    {
        "browserslist": [
            "last 2 version",
            "not dead",
            "> 0.2.%"
        ]
    }
    
  • 方案二:.browserslistrc文件

    > 0.5%
    last 2 version
    not dead
    
如果没有配置,那么也会有一个默认配置
browserslist.defaults = [
    '> 0.5%',
    'last 2 version',
    'Firefox ESR',
    'not dead'
]

认识PostCSS工具

什么是PostCSS?
  • PostCSS是一个通过JavaScript来转换样式的工具
  • 这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀,css样式的重置

在webpack使用PostCSS工具

  • 安装postcss-loader npm install postcss-loader -D

  • 安装postcss的插件,postcss-preset-env npm install postcss-preset-env -D

  • 配置

    module.exports = {
        // ... 省略
        module: {
            rules: [
                {
                    test: /.css$/,
                    use: [
                        'style-loader',
                        {
                            loader: 'css-loader',
                            options: {
                                importLoaders: 1 // 配置在css-loader之前有多少个加载器
                            }
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                postcssOptions: {
                                    plugins: [
                                        require('postcss-preset-env')
                                    ]
                                }
                            }
                        }
                    ]
                }
            ]
        }
    }
    
单独的postcss配置文件
  • 我们可以将postcss的配置信息放到一个单独的文件

  • 根目录下创建postcss.config.js

    module.exports = {
        plugins: [
            require('postcss-preset-env')
        ]
    }
    

加载和处理其他资源

加载图片资源
  • 在项目中引入图片,有两种常见的方式

    • img元素:设置src属性
    • 其他元素:设置background-image的css属性
import zznh from './img/zznh.png'
const zznhImg = new Image()
zznhImg.src = zznh
element.appendChild(zznhImg)
  • 不管是通过css还是img标签引入的图片在进行打包时都会报错
file-loader
  • 要处理jpg,png等格式的图片,我们也需要有对应的loader:file-loader

    • file-loader的作用就是帮助我们处理import/require()方式引入的一个文件资源,并且会将它放到我们输出的文件夹中
  • 安装file-loader : npm install file-loader -D

module.exports = {
    // ... 省略
    module: {
        rules: [
            {
                test: /.(png|jpe?g|gif|svg)$/i,
                use: {
                    loader: 'file-loader'
                }
            }
        ]
    }
}
  • 文件的名称规则
    • 有时候我们处理后的文件名称需要按照一定的规则进行显示:

      • 比如保留原来的文件名,扩展名,同时为了防止重复,包含一个hash值等
    • 这个时候我们可以使用PlaceHolders(占位符) 来完成

    • 常见的placeHolder

      • [ext] : 处理文件的扩展名
      • [name] : 处理文件的名称
      • [hash] : 文件的内容,使用MD4的散列函数处理,生成一个128位的hash值(32个16进制)
      • [contentHash] : 在file-loader中和[hash]结果是一致的(在其他的地方会不一样)
      • [hash: < length >] : 截取hash的长度,默认32个字符太长了
      • [path] : 文件相对于webpack配置文件的路径
  • 设置文件名称
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /.(jpe?g|png|svg|gif)$/i,
                use: {
                    loader: 'file-loader',
                    options: {
                        // 这样配置后打包的图片资源会放在img文件夹下
                        name: 'img/[name].[hash:8].[ext]' 
                        // 下面的写法也可以让打包后的文件保存在img文件夹下
                        outputPath: 'img',  // 设置打包后存放的文件夹
                        name: '[name].[hash:8].[ext]'
                    }
                }
            }
        ]
    }
}
url-loader
  • url-loader和file-loader的工作方式是相似的,但是可以将较小的文件,转成base64的URI
  • 安装url-loader npm install url-loader -D
  • 配置url-loader (这样配置打包后显示结果是一样的,但是我们会看不到打包后的图片文件,这是因为默认情况下,url-loader会将所有的图片文件转成base64编码)
const path = require('path')
​
module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './build')
    },
    module: {
        rules: [
            {
                test: /.(jpg|svg|gif|png)$/i,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name].[hash:8].[ext]',
                        outputPath: 'img'
                    }
                }
            }
        ]
    }
    
}
​
​
  • url-loader的limit

    • 开发中我们往往是小的图片需要转化,但是大的图片直接使用即可

      • 这是因为小的图片转换base64之后可以和页面一起请求,减少不必要的请求过程
      • 大的图片也进行转换,反而会影响页面的请求速度
  • 通过limit属性,可以设置转换限制

module.exports = {
    // ... 省略
    module: {
        rules: [
            test: /.(png|jpg|svg|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    outputPath: 'img',
                    name: '[name].[hash:8].[ext]',
                    limit: 100 * 1024  // 小于100kb的会被转换为base64,大于则不会
                }
            }
        ]
    }
}

asset module type的介绍

  • 当前我们使用的webpack版本是webpack5

    • 在webpack5之前,加载这些资源我们需要使用一些loader,比如raw-loader,url-loader
    • 在webpack5之后,我们可以直接使用资源模块类型(asset module type),来替代loader
  • 资源模块类型,通过添加4种新的模块类型,来替代所有的这些loader

    • asset/resource:发送一个单独的文件并导出URL。之前通过使用file-loader实现
    • asset/inline:导出一个资源的dataURI。之前通过使用url-loader实现
    • asset/soruce:导出资源的源代码。之前通过raw-loader实现
    • asset: 在导出一个data URI和发送一个单独的文件之间自动选择。之前通过url-loader,并且配置资源体积限制实现

asset module type的使用

  • 加载图片,我们可以使用下面的方式
module.exports = {
    // ... 省略
    module: {
        rules: [
            {
                test: /.(jpe?g|svg|png|gif)$/,
                type: 'asset/resource'
            }
        ]
    }
}
  • 如何可以自定义文件的输出路径和文件名呢?

    • 方式一:修改output,添加assetModuleFilename属性
    • 方式二:在rule中,添加一个generator属性,并且设置filename
// 方式一
module.exports = {
    output: {
        filename: 'js/bundle.js',
        path: path.resolve(__dirname, './dist'),
        assetModuleFilename: 'img/[name].[hash:6][ext]' // 这里的[ext]包含了 . 所以不用加.
    }
}
module.exports = {
    // ... 省略
    module: {
        rules: [
            {
                test: /.(jpe?g|svg|png|gif)$/,
                type: 'asset/resource',
                generator: {
                    filename: 'img/[name].[hash:6][ext]'
                }
            }
        ]
    }
}
url-loader的limit效果
  • 将type改为 "asset"或者"asset/inline"
  • 添加一个parser属性,并且制定dataUrl的条件,添加maxSize属性
// ... 省略
rules: [
    {
        test: /.(jpe?g|svg|png|gif)$/,
        type: 'asset',
        generator: {
            filename: 'img/[name].[hash:6][ext]'
        },
        parser: {
            dataUrlCondition: {
                maxSize: 100 * 1024
            }
        }
    }
]

认识plugin

  • webpack的另一个核心是plugin

  • loader和plugin有什么区别:

    • loader是用于特定模块类型进行转换
    • plugin可以用于执行更加广泛的任务,比如打包优化,资源管理,环境变量注入等等

CleanWebpackPlugin

  • 在前面我们每修改配置重新打包时,都需要手动删除dist文件夹
  • CleanWebpackPlugin这个插件可以帮助我们完成这个操作
安装CleanWebpackPlugin npm install clean-webpack-plugin -D
  • 配置插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
​
module.exports = {
    // ... 省略
    plugins: [
        new CleanWebpackPlugin()
    ]
}
​

HtmlWebpackPlugin

  • 打包后的文件夹是没有html文件的
  • 项目部署时,必然也是需要有对应的入口文件index.html
  • 所以我们也需要对index.html进行打包处理
  • 对html进行打包处理我们可以使用另外一个插件HtmlWebpackPlugin

安装HtmlWebpackPlugin npm install html-webpack-plugin -D

const HtmlWebpackPlugin = require('html-webpack-plugin')
​
module.exports = {
    // ... 省略
    plugins: [
        new HtmlWebpackPlugin({
            title: 'webpack案例',
            template: './public/index.html'
        })
    ]
}

DefinePlugin

  • DefinePlugin允许在编译时创建配置的全局常量,是一个webpack内置的插件(不需要单独安装)
const { DefinePlugin } = require('webapck')
​
module.exports = {
    // ... 省略
    plugins: [
        new DefinePlugin({
            BASE_URL: "'./'"  // 这里如果只写一个双引号,会将双引号里的内容当成一个变量去寻找
        })
    ]
}

CopyWebpackPlugin

  • 在vue打包过程中,如果我们将一些文件放到public的目录下,那么这个文件夹会被复制到dist文件夹中

    • 这个复制的功能,我们可以使用CopyWebpackPlugin来完成
  • 安装CopyWebpackPlugin插件

    • npm install copy-webpack-plugin -D
  • 接下来配置CopyWebpackPlugin即可:

    • 复制的规则在patterns中设置
    • from:设置从哪一个源中开始复制
    • to:复制到的位置,可以省略,会默认复制到打包的目录下
    • globOptions:设置一些额外的选项,其中可以编写需要忽略的文件
const CopyWebpackPlugin = require('copy-webpack-plugin')
​
module.exports = {
    // ... 省略
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: 'public',
                    globOptions: {
                        ignore: [
                            '**/index.html'
                        ]
                    }
                }
            ]
        })
    ]
}

source-map

mode配置
  • mode配置选项,可以告知webpack使用响应式模块的内置优化

    • 默认值production
    • 可选值: 'none' | 'development' | 'production'
  • 设置完mode选项后,webpack会默认帮助我们配置很多选项、

认识source-map
  • 我们的代码运行在浏览器上,是通过打包压缩

    • 也就是真实跑在浏览器上的代码,和我们编写的代码其实是有差异的
    • 但是,当代码报错需要调试时,调试转换后的代码是很困难的
  • 如何可以调试这种转换后不一致的代码呢?答案就是source-map

    • source-map是从已转换的代码,映射到原始的源文件
    • 使浏览器可以重构原始源并在调试器中显示重建的原始源
如何使用source-map
  • 根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成source-map

  • 在转换后的代码,最后添加一个注释,它指向source-map

    //# sourceMappingURL=common.bundle.js.map

  • 浏览器会根据我们的注释,查找响应的source-map,并且根据source-map还原我们的代码

分析source-map

目前的source-map文件大概是原始文件的两倍

目前的source-map长什么样子?

  • version:当前使用的版本
  • sources: 从哪些文件转换过来的source-map和打包的代码(最初始的文件)
  • names:转换前的变量和属性名称(如果使用的是development模式,则不需要保留转换前的名称)
  • mappings:source-map用来和源文件映射的信息
  • file:打包后的文件(浏览器加载的文件)
  • sourceContent:转换前的具体代码信息(和sources是对应关系)
  • sourceRoot:所有sources相对的根目录
生成source-map
  • 通过devtool:设置生成source-map

  • 不生成source-map的值

    • false:不使用source-map,也就是没有任何source-map相关的内容

    • none:production模式下的默认值,不生成source-map

    • eval:development模式下的默认值,不生成source-map

      • 但是它会在eval执行的代码中,添加 // sourceURL=;
      • eval的效果图如下

image-20210914191352509.png 生成source-map值:

  • source-map

    • 生成一个独立的source-map文件,并且在打包后的js文件中有一个注释,指向source-map文件

    image-20210914191648167.png

    image-20210914191654919.png

  • eval-source-map

    • 会生成source-map,但是source-map是以DataURL添加到eval函数的后面

image-20210914191941481.png

  • inline-source-map

    • 会生成source-map,但是source-map是以DataUrl添加到打包后的js文件中

image-20210914192136863.png

  • cheap-source-map

    • 和设置source-map效果差不多,会生成source-map但是会更加高效一些,因为它没有生成列映射
    • 在开发中,我们只需要行信息通常就可以定位到错误了

image-20210914192543201.png

image-20210914192551519.png

  • cheap-module-source-map

    • 会生成source-map,类似于cheap-source-map,但是对源自loader的source-map处理会更好
    • 如果loader对我们源码进行了特殊的处理,比如babel

image-20210914193444798.png

image-20210914193501246.png

  • hidden-source-map

    • 会生成source-map,但是不会对source-map文件进行引用
    • 相当于删除了打包后文件中对source-map的引用注释
    • 如果我们手动添加进来,那么source-map就会生效
  • nosources-source-map

    • 会生成source-map,但是生成的source-map只有错误信息的提示,不会生成源代码文件

image-20210914193924432.png

多个值的组合
  • 事实上,webpack提供给我们26个值,是可以进行多组合的

  • 组合规则如下:

    • inline-|hidden-|eval:三个值选一
    • nosources:可选值
    • cheap可选值,并且可以跟随module的值
在开发中,最佳的实践是什么?
  • 开发阶段:推荐使用source-mapcheap-module-source-map

    • 这分别是vue和react使用的值
  • 测试阶段:和开发阶段一致

    • 因为测试阶段我们也希望在浏览器下看到正确的错误提示
  • 发布阶段:false,不写

Babel

为什么需要babel?
  • 在开发中我们想要使用ES6+语法,想要使用Typescript,想要编写jsx,都是离不开babel的
babel是什么?
  • babel是一个工具链,主要用于将一些浏览器不认识的语法转换为浏览器能够运行的js代码
  • 包括:语法转换代码转换polyfill:将浏览器没有的新语法进行打补丁
babel命令行使用
  • babel本身可以作为一个独立的工具(和postcss一样),可以单独使用

  • 如果我们希望在命令行尝试使用babel,需要安装如下库:

    • @babel/core :babel的核心代码,必须安装
    • @babel/cli:可以让我们在命令行中使用(如果只需要在webpack中使用可以不安装)
    • npm install @babel/cli @babel/core
  • 使用babel来处理我们的源代码:

    • src:源文件的目录
    • --out-dir:指定要输出的文件夹dist
    • npx babel src --out-dir dist (--plugins=“插件名”)(--presets=@babel/preset-env)
babel的底层原理
  • babel是如何将我们的一段代码转换成另外一段代码的呢?

    • 从一种源代码转换成另一种源代码(目标语言),这是编译器的工作
    • 事实上我们可以将babel看成是一个编译器
  • babel也拥有编译器的工作流程

    • 解析阶段(parsing)
    • 转换阶段 (transformation)
    • 生成阶段 (code generator)

image-20210915205253217.png

image-20210915205307270.png

babel-loader
  • 在实际开发中,我们通常会在构建工具中通过配置babel来对其进行使用,比如在webpack中

  • 需要安装相关依赖

    • npm install babel-loader @babel/core -D
// 省略...
module: {
    rules: [
        {
            test: /.m?js$/,
            use: {
                loader: 'babel-loader'
            }
        }
    ]
}
指定babel使用插件
  • 如果没有使用插件,babel是不会对我们的代码进行转换的
// 省略...
module: {
    rules: [
        {
            test: /.m?js$/,
            use: {
                loader: 'babel-loader',
                options: {
                    plugins: [
                        '@babel/plugin-transform-block-scoping', // 转换块级作用域的
                        '@babel/plugin-transform-arrow-functions' // 转换箭头函数的
                    ]
                }
            }
        }
    ]
}
babel-preset
  • 在开发中我们要转换的代码,如果需要一个一个去安装插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并将其传递给babel

  • 常见的预设

    • env
    • react
    • Typescript
  • 安装preset-env

npm install @babel/preset-env -D

  • 配置
// 省略 ...
{
    test: /.m?js$/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                ['@babel/preset-env']
            ]
        }
    }
}
设置目标浏览器
// 省略 ...
{
    test: /.m?js$/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                ['@babel/preset-env', {
                    targets: ['last 2 version', 'not dead']
                }]
            ]
        }
    }
}
  • 在前面中我们已经使用了browserslist工具,我们可以对比一下不同的配置,打包的区别

image-20210915211452507.png

  • 如果我们同时设置了targets和browserslisttargets属性会覆盖browserslist
  • 在开发中,更推荐通过browserslist来配置,因为类似于postcss工具,与会使用browserslist,进行统一的浏览器适配
babel的配置文件
  • 我们可以将babel的配置文件放到一个独立的文件中,babel给我们提供了两种配置文件的编写

    • babel.config.json(或者.js, .cjs, .mjs)文件
    • .babelrc.json(或者.js, .cjs, .mjs)文件
  • 它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、element-plus、umi等);

    • babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的;
    • babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐;
认识polyfill
  • polyfill是什么?

    • 像是填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript
  • 为什么需要polyfill?

    • 比如在开发中我们用到了一些新特性,比如Promise,Symbol等
    • 但是浏览器根本不认识这些新特性,所以直接运行必然会报错
    • 这个时候我们就可以使用polyfill来填充或者说打一个补丁,那么浏览器就能运行该代码了
如何使用polyfill?
  • 在babel7.4.0之前(目前7.15.0),可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了
  • babel7.4.0之后,可以通过单独引入core-jsregenerator-runtime来完成polyfill

npm install core-js regenerator-runtime

{
    test: /.m?js$/,
    exclude: /node_modules/, // 使用babel时,一般不会包含node_modules下的文件
    use: 'babel-loader'
}
使用polyfill,babel.config.js的配置
  • 我们需要在babel.config.js文件中进行配置,给preset-env配置一些属性

  • useBuiltIns:设置以什么方式来使用polyfill

    • false

      • 打包后的文件不使用polyfill来进行适配
      • 并且这个时候是不需要设置corejs属性的
    • usage

      • 会根据源代码出现的语言特性,自动检测所需要的的polyfill
      • 这样可以确保最终包里的polyfill数量最小化,打包的包相对会小一些
      • 可以设置corejs属性来确定使用的corejs版本
      • // babel.config.js
        module.exports = {
            presets: [
                ['@babel/preset-env', {
                    useBuiltIns: 'usage',
                    corejs: 3.17
                }]
            ]
        }
        
    • entry

      • 如果我们依赖的某一个库使用了某些polyfill的特性,但是因为我们使用的是usage,所以之后用户浏览器可能会报错
      • 所以如果担心发生这种情况,可以使用entry
      • 并且需要在入口文件添加import 'core-js/stable'; import 'regenerator-runtime/runtime'
      • 这样做会根据browserslist目标导入所有的polyfill,但是对应的包也会变大
      • // babel.config.js
        module.exports = {
            presets: [
                ['@babel/preset-env', {
                    useBuiltIns: 'entry',
                    corejs: 3.17
                }]
            ]
        }
        ​
        // 入口文件
        import 'core-js/stable'
        import 'regenerator-runtime/runtime'
        
  • corejs:设置corejs的版本,目前使用的较多的是3.x版本,目前安装的默认版本是3.17.3

    • 另外corejs可以设置是否对提议阶段的特性进行支持
    • 设置proposals属性为true即可
认识plugin-transform-runtime
  • 前面我们使用的polyfill,默认情况时添加的所有属性都是全局的

    • 如果我们正在编写一个工具库,这个工具库需要使用polyfill
    • 别人在使用我们的工具是,工具库通过polyfill添加的特性,可能会污染它们的代码
    • 所以,当编写工具库时,babel更推荐我们使用一个插件: @babel/plugin-transform-runtime来完成polyfill的功能
  • 使用plugin-transform-runtime

    • npm install @babel/plugin-transform-runtime -D
    • npm install @babel/runtime-corejs3
module.exports = {
    presets: [
        ['@babel/preset-env']
    ],
    plugins: [
        ['@babel/plugin-transform-runtime', {
            
        }]
    ]
}
打包react代码
  • 在编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的

  • 对react jsx代码进行处理需要如下插件

    • @babel/plugin-syntax-jsx
    • @babel/plugin-transform-react-jsx
    • @babel/plugin-transform-react-display-name
  • 但是开发中,我们不需要一个个安装这些插件,我们依然可以使用preset来配置:

    • npm install @babel/preset-react -D
    module.exports = {
        presets: [
            ['@babel/preset-react']
        ]
    }
    
TypeScript的编译
  • 可以通过typescript的compiler来转换成JavaScript

    • npm install typescript -D
    • 另外typescript的编译配置信息,我们通常会写一个tsconfig.json文件 tsc --init
    • 通过运行 npx tsc来编译自己的ts代码
ts-loader
  • 如果希望在webpack中使用typescript,那么我们可以使用ts-loader来处理ts文件
  • {
        test: /.ts$/,
        exclude: /node_modules/,
        use: [
            'ts-loader'
        ]
    }
    
使用babel-loader编译typescript代码
  • 安装@babel/preset-typescript npm install @babel/preset-typescript -D
{
    test: /.ts$/,
    exclude: /node_modules/,
    use: 'babel-loader'
}
​
// babel.config.js
module.exports = {
    presets: [
        ['@babel/preset-typescript']
    ]
}
ts-loader 和 babel-loader选择
  • 在开发中我们应该选择ts-loader还是babel-loader

  • 使用ts-loader

    • 只能将ts转换成js
    • 如果我们还希望在这个过程中添加对应的polyfill,ts-loader是无法完成的
  • 使用babel-loader

    • 可以将ts转为js,并且也能实现polyfill的功能
    • 但是babel-loader在编译过程中,不会对类型错误进行检测
  • 也就是说,我们可以使用babel来完成代码转换,使用tsc来进行类型检测

    • package.json文件中的scripts中添加两个脚本,用于类型检测

    • image-20210916100235021.png

    • 我们执行 npm run type-check可以对ts代码的类型进行检测

    • 执行npm run type-check-watch可以实时的检测类型错误

    • 但是在vscode中,但我们写ts代码时,编译器会自动帮助我们进行代码检测

加载vue2文件(加载vue3文件看vue3笔记)
  • 安装相关依赖

    • npm install vue-loader -D
    • npm install vue-template-compiler -D
  • 配置webpack

// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    // 省略...
    module: {
        rules: [
            {
                test: /.vue$/,
                use: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}

DevServer和HMR

为什么要搭建本地服务器
  • 目前开发的代码,为了能够运行需要两个操作

    • npm run build,编译相关的代码
    • 通过live server或者浏览器打开index.html代码,查看效果
  • 这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成编译和展示

  • 为了完成自动编译,webpack提供了几种可选的方式

    • webpack watch mode
    • webpack-dev-server
    • webpack-dev-middleware
webpack watch
  • webpack提供了watch模式

    • 在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译
    • 我们不需要手动去运行npm run build指令了
  • 如何开启watch?两种方式

    • 直接在webpack.config.js 文件中,添加 watch: true
    • 在启动webpack的命令中,添加--watch的标识
"scripts": {
    "build": "webpack",
    "watch": "webpack --watch"
}
webpack-dev-server
  • 使用watch的方式可以监听到文件的变化,但是事实上它本身没有自动刷新浏览器的功能

    • 在vscode中使用live-server可以实现浏览器自动刷新的功能
    • 但是,我们希望在不使用live-server的情况下,可以具备live reloading(实时重新加载)的功能
  • 安装webpack-dev-server npm install webpack-dev-server -D

  • 添加一个新的scripts脚本

"serve": "webpack serve"
  • webpack-dev-server 在编译后不会写入到任何输出文件,而是将bundle文件保存到内存中

    • 事实上webpack-dev-server使用了一个库叫memfs(memory-fs webpack自己写的)
webpack-dev-middleware
  • 默认情况下,webpack-dev-server已经帮助我们做好了一切

    • 比如通过express开启一个服务,比如HMR(热模块替换)
    • 如果我们想要有更好的自由度,可以使用webpack-dev-middleware
  • 什么是webpack-dev-middleware

    • webpack-dev-middleware是一个封装器,可以把wepack处理过的文件发送到一个server
    • webpack-dev-server在内部使用了它,然而它也可以作为一个单独的package来使用,以便根据需求进行更多自定义设置
  • 安装express和webpack-dev-middleware npm install webapck-dev-middle express -D

  • 创建一个js文件(例如:server.js)

const express = require('express')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')
​
const app = express()
// 记载配置信息
const config = require('你的webpack配置文件的路径')
// 将配置信息传递给webpack进行编译
const compiler = webpack(config)
​
// 将编译后的结果传递给webpackDevMiddleware这个中间件处理
app.use(webpackDevMiddleware(compiler))
​
app.listen(8000, () =>{
    console.log('服务器已经在8000端口启动')
})
​
  • 通过node server.js 启动服务器
认识模块热替换(HMR)
  • 什么是HMR?

    • HMR的全称是Hot Module Replacement,翻译为热模块替换
    • 热模块替换是指在应用程序运行过程中,替换,添加,删除模块,而无需重新刷新整个页面
  • HMR通过如下几种方式,来提高开发的速度

    • 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
    • 只更新需要变化的内容,节省开发的时间
    • 修改了css,js源码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式
  • 如何使用HMR呢?

    • 默认情况下,webpack-dev-server已经支持HMR,我们只需要开启即可
    • 在不开启HMR的情况下,当我们修改了源代码之后,整个页面会重新自动刷新,使用的是 live reloading(实时重新加载)
  • 如何开启HMR?


// 在webpack.config.js文件中修改
module.exports = {
    mode: '',
    entry: '',
    // ...
    devServer: {
        hot: true   // 开启HMR
    }
}

image-20210927102209147.png

  • 但是会发现,当我们修改了某一个模块的代码时,依然刷新的整个页面

    • 这是因为我们需要去指定哪些模块发生更新时,进行HMR
// 在webpack打包的入口文件中,在其他js文件中这样写是不能开启HMR的
if (module.hot) {
    module.hot.accept('你想要进行HMR的文件', () => {
        console.log('文件更新了')
    })
}
框架的HMR
  • 开发vue或者react项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢?

    • 事实上vue开发中,我们使用vue-loader,此loader支持vue组件的HMR,提供开箱即用的体验
    • 比如react开发中,有React Hot Loader,实时调整react组件(目前官方已经弃用了,改成react-refresh
react的HMR
  • react是借助react-refresh来实现的
  • 安装依赖 npm install @pmmmwh/react-refresh-webpack-plugin react-refresh -D
  • 修改webpack.config.js文件和babel.config.js文件
// webpack.config.js文件
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
module.exports = {
    // ...其他配置
    plugins: [
        new ReactRefreshWebpackPlugin() // 执行npm run build时,不能有该插件,否则会报错
    ]
}
// babel.config.js 文件
module.exports = {
    plugins: [
        ['react-refresh/babel']
    ]
}
vue的HMR
  • vue的加载我们需要vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR处理
  • 安装加载vue所需要的的依赖 npm install vue-loader vue-template-compiler -D
  • 配置webpack.config.js
// webpack.config.js文件
// 有两种方法导入VueLoaderPlugin,本质上都是一样的
// const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /.vue$/,
                use: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}
HMR的原理
  • HMR的原理是什么呢?如何可以做到只更新一个模块中的内容呢?

    • webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket)
    • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
  • HMR Socket Server,是一个socket的长连接

    • 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端)
    • 当服务器监听到对应的模块发生变化时,会生成两个文件 .json(manifest文件).js文件(update chunk)
    • 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器)
    • 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新

image-20210927150011381.png

output的publicPath和devServer的publicPath

  • output中的publicPath属性,该属性是指定index.html文件打包引用的一个基本路径

    • 它的默认值是一个空字符串,所以我们打包后引入js文件时,路径是bundle.js
    • 在开发中,我们也将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名加路径去请求对应的资源
    • 如果我们希望在本地直接打开html文件来运行,会将其设置为 ./ ,路径为 ./bundle.js,可以根据相对路径去寻找资源
  • devServer中的publicPath,该属性时指定本地服务所在的文件夹

    • 它的默认值是 / ,也就是我们直接访问端口即可访问其中的资源
    • 如果我们将其设置为 /abc,那么我们需要通过**http://locallhost:8080/abc**才能访问到对应的打包后的资源
    • 并且这个时候,我们其中的bundle.js通过**http://locallhost:8080/bundle.js是无法访问的
    • 所以建议将output中的publicPath和devServer中的publicPath设置为相同

devServer常见的配置

contentBase
  • 这个属性对于打包后的资源是没有什么作用的,它的作用是在devServer创建一个本地服务时,如果我们打包后的资源又依赖其他的资源,那么就需要指定从哪里来查找这个内容
  • 例如在给html-webpack-plugin的模板HTML中有引用其他资源

image-20210927160632502.png

  • 通过npm run serve 后会发现这个资源加载不到,默认情况下会在启动项目的根目录进行查找

image-20210927160855148.png

  • 而根目录下并没有aaa.js文件,该文件是被我放在src目录下,所以加载不了该文件
  • 如果我们想要能加载aaa.js这个文件,这个时候就可以通过设置contentBase来实现
module.exports = {
    // ...
    devServer: {
        contentBase: './src'
    }
}
  • 重新执行npm run serve

image-20210927161226623.png

  • 这个时候查找aaa.js文件时,会去根目录下的src文件夹查找,而aaa.js文件就在src文件夹下,所以能加载到aaa.js文件
watchContentBase
  • 默认情况下,webpack-dev-server是不会监听上面aaa.js文件的修改,如果想要能监听这个文件的修改,就可以将watchContentbase设置为true,
  • 设置完后,当修改了aaa.js文件后,webpack-dev-server会自动帮助我们刷新浏览器
hotOnly,host配置
  • hotOnly是当代码编译失败时,是否刷新整个页面

    • 默认情况下当代码编译失败修复后,我们会重新刷新整个页面
    • 如果不希望重新刷新整个页面,可以设置hotOnly为true
  • host设置主机地址

    • 默认值是localhost
    • 如果希望其他地方也可以访问,可以设置0.0.0.0
module.exports = {
    // ...
    devServer: {
        hotOnly: true,
        host: '0.0.0.0'
    }
}
port,open,compress
  • port设置监听的端口号,默认情况是8080

  • open是否打开浏览器

    • 默认值是false,设置为true时会自动打开浏览器
    • 也可以设置为类似于Google Chrome等值
  • compress是否为静态文件开启gzip compression

    • 默认值是false,可以设置为true
module.exports = {
    // ...
    devServer: {
        open: true,
        port: 8000,
        compress: true
    }
}
proxy代理
  • proxy是我们开发中非常常用的一个配置选项,它的目的是设置代理来解决跨域访问的问题。(浏览器同源策略:当两个url的协议,主机,端口号都相同时,则这两个url为同源,可以互相发送网络请求。当三个其中一个不同时,则会产生跨域问题)

    • 我们可以将请求先发送到一个代理服务器代理服务器和API服务器没有跨域问题,就可以解决我们的跨域问题了
module.exports = {
    // ...
    devServer: {
        proxy: {
            '/api': {
                target: '你要请求的baseUrl',
                pathRewrite: { // 路径重写
                    '^/api': ''
                },
                secure: false, //false时可以代理没有证书的https
                changeOrigin: true, // 设置为true时,客户端发出的请求头端口会和服务器一致,如果为false端口就是客户端的端口,有的服务器会检查端口,如果端口不一致,不返回数据
            }
        }
    }
}
  • 发送请求时url变为: "api/参数"
historyApiFallback
  • historyApiFallback是开发中一个非常常见的属性,它的主要作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误

  • boolean值:默认是false

    • 如果设置为true,那么在刷新时,返回404错误时,会自动返回index.html的内容
  • object类型的值,可以配置rewrites属性

    • 可以配置from来配置路径,决定要跳到哪一个页面
module.exports = {
    // ...
    devServer: {
        // historyApiFallback: true,
        historyApiFallback: {
            rewrites: [
                { from: /^/$/, to: '/views/landing.html' },
                { from: /^/subpage/, to: '/views/subpage.html' },
                { from: /./, to: '/views/404.html' },
            ],
        }
    }
}

resolve模块解析

  • webpack能解析三种文件路径:

    • 绝对路径

      • 由于已经获的文件的绝对路径,因此不需要再做进一步的解析
    • 相对路径

      • 在这种情况下,使用importrequire的资源文件夹所处的目录,被认为是上下文目录
      • 在imort/require中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径
    • 模块路径

      • 在resolve.modules中指定的所有目录检索模块

        • 默认值是['node_modules'] ,所以默认会从node_modules中查找文件
      • 可以通过设置别名的方式来替换初始模块路径

webpack查找路径时,确定是文件还是文件夹
  • 如果是一个文件:

    • 如果文件具有扩展名,则直接打包文件
    • 否则,将使用resolve.extensions选项作为文件扩展名解析
  • 如果是一个文件夹

    • 会在文件夹中根据resolve.mainFiles配置选项中指定的文件顺序查找;

      • resolve.mainFiles的默认值是['index'],这也就是为什么我们路径是一个文件夹时,会指定去查找该文件夹下的index类型文件
      • 在根据resolve.extensions来解析扩展名
extensions和alias配置
  • extensions是解析到文件时自动添加扩展名

    • 默认值是: ['.wasm', '.mjs', '.js', 'json']
    • 所以如果我们的代码中想要添加加载 .vue,或者.jsx或者.ts等文件时,我们必须自己写上扩展名
  • 另一个非常好用的功能是配置别名alias

    • 当我们的项目目录结构比较深时,获取一个文件的路径可能需要 ../../../ 这种路径片段
    • 所以我们可以给某些常见的路径起一个别名
const path = require('path')
module.exports = {
    // ...
    resolve: {
        extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.ts', '.vue'],
        alias: {
        "@": path.resolve(__dirname, './src')
        }
    }
}
resolve的配置还有很多可以查看官网

环境分离和代码分离