Webpack-学习笔记03

loading 2023年01月14日 102次浏览

1. source-map

我们在开发中需要经常用到debug功能。之前我们通过webpack将我们的源码打包成了bundle.js,实际上客户端(浏览器)读取的是打包后的bundle.js ,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle的内容。

我们如何将报错信息(bundle错误的语句及其所在行列)映射到源码上?答案就是通过source-map。

webpack已经内置了source-map,我们通过简单的配置就能够开启它:

devtool: 'source-map'

其中source-map有以下前缀可以选择:

  • inline:不生成映射关系文件,打包进main.js

  • cheap: 1.只精确到行,不精确到列,打包速度快 2.只管业务代码,不管第三方模块

  • module:不仅管业务代码,而且管第三方代码

  • eval:执行效率最快,性能最好

不过需要注意,生产环境下一般不需要开启source-map ,主要有两点原因:

  • 通过bundle和sourcemap文件可以反编译出源码,有暴露源码的风险
  • sourcemap文件的体积相对比较巨大,这跟我们生产环境的追求不同。(生产环境追求更小更轻量的bundle)

2. devServer

开发环境下,我们往往需要启动一个web服务,方便我们模拟一个用户从浏览器中访问我们的web服务,读取我们的打包产物,以观测我们的代码在客户端的表现。Webpack内置了这个功能:

    devServer: {
        // 路径
        static: path.resolve(__dirname, './dist'),

        // 是否在服务端开启gzip压缩代码
        compress: false,

        // 启动端口
        port: 3000,

        // 使得自己的devServer服务能被同一局域网内的人通过ip访问到
        host: '0.0.0.0',

        // 响应头
        headers: {
            'X-Access-Token': 'abc123'
        },

        // 代理跨域 如果路径包含/api则代理到9000端口
        proxy: {
            '/api': 'http://localhost:9000'
        },

        // 是否开启https 两种方法
        // https: true
        http2: true,

        // 404时跳回指定的地址 设置为true即跳回根页面 可以根据需求作更多配置
        historyApiFallback: true,

        // 模块热替换 默认为true
        // 实现在应用程序运行过程中,增删改模块而无需重新加载整个页面
        hot: false,

        // 模块热加载 默认为true
        // 实现文件更新时,自动刷新我们的服务和页面
        liveReload: false
    },

重点掌握通过proxy代理跨域,通过historyApiFallback跳转地址,模块热替换和热加载等内容。

proxy实现跨域的原理是什么?

proxy工作原理实质上是利用http-proxy-middleware这个http代理中间件,实现请求转发给其他服务器。

在开发阶段, webpack-dev-server 会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost的一个端口上,而后端服务又是运行在另外一个地址上

所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题

通过设置webpack proxy实现代理请求后,相当于浏览器与服务端中添加一个代理者

当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地。

注意:服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制

2.1 模块热加载热替换js

上面的config配置可以实现CSS的热加载热替换,如果要实现js中的热更新还要在js文件中写入对应热更新代码,比如:

if (module.hot) {
  module.hot.accept(’./library.js’, function() {
    // Do something with the updated library module…
  });
}

library.js

document.querySelector('#box').innerHTML = '<input type="text" value="test23456" />'

3. eslint

eslint是用来扫描我们所写的代码是否符合规范的工具。 往往我们的项目是多人协作开发的,我们期望统一的代码规范,这时候可以让eslint来对我们进行约束。 严格意义上来说,eslint配置跟webpack无关,但在工程化开发环境中,它往往是不可或缺的。

npm i eslint -D

//安装好后执行
npx eslint --init

会生成这么一个文件:

{
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": "airbnb-base",
    "overrides": [
    ],
    "parserOptions": {
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "rules": {
    }
}

我们现在创建文件夹src,随便写一个文件app.js:

console.log('hello eslint');

然后在控制台中执行检察:

npx eslint ./src

可以在配置文件中忽略对no-console的提示:

    "rules": {
        "no-console": 0
    }

但是这样检查太麻烦了,我们可以直接通过vscode的扩展下载eslint:

此时如果犯语句后不写分号之类的错误vscode就会自动报错了

如果即不想用命令行这么麻烦,又不想安装插件,那么还可以通过webpack来实现:

在webpack.config.js中这么写:

  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader', 'eslint-loader'],
      },
    ],
  },

4. 外部扩展

通过externals实现通过外部cdn加载第三方库

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

module.exports = {
  mode: 'development',
  entry: './app.js',
  plugins: [
    new HtmlWebpackPlugin()
  ],
  
  // 类型
  externalsType: 'script',
  externals: {
    // 链接和别名
    jquery: [
      'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js',
      '$'
    ]
  }
}

5. webpack + web worker

有时我们需要在客户端进行大量的运算,但又不想让它阻塞我们的js主线程。你可能第一时间考虑到的是异步。但事实上,运算量过大(执行时间过长)的异步也会阻塞js事件循环,甚至会导致浏览器假死状态。这时候,HTML5的新特性WebWorker就派上了用场。

html5之前,打开一个常规的网页,浏览器会启用几个线程?
一般而言,至少存在三个线程(公用线程不计入在内):
分别是js引擎线程(处理js)、GUI渲染线程(渲染页面)、浏览器事件触发线程(控制交互)。

webWorkers提供了js的后台处理线程的API,它允许将复杂耗时的单纯js逻辑处理放在浏览器后台线程中进行处理,让js线程不阻塞UI线程的渲染。

new Worker("someWorker.js");

但是常规模式下,webpack工程化环境只会打包出一个bundle.js,那我们的worker脚本怎么办?

webpack5内置了处理这种情况的功能:

创建一个脚本worker.js

self.onmessage = ({ data: { question } }) => {
    self.postMessage({
        answer: 42,
    })
}

在index.js中使用

const worker = new Worker(new URL('./work.js', import.meta.url));
worker.postMessage({
    question:
        'hi,那边的worker线程,请告诉我今天的幸运数字是多少?',
});
worker.onmessage = ({ data: { answer } }) => {
    console.log(answer);
};

6. 多页面应用

6.1 entry

    entry: {
        main: {
            import: ['./src/app2.js', './src/app.js'],
            dependOn: 'lodash',
            filename: 'chanel1/[name].js'
        },
        main2: {
            import: './src/app3.js',
            dependOn: 'lodash',
            filename: 'chanel2/[name].js'
        },
        lodash: {
            import: 'lodash',
            filename: 'common/[name].js'
        }
    }

6.2 plugins

    plugins: [
        new HtmlWebpackPlugin({
            title: '多页面应用',
            template: './index.html',
            inject: 'body',
            filename: 'chanel1/index.html',
            chunks: ['main', 'lodash'],
            publicPath: 'http://www.b.com/'
        }),

        new HtmlWebpackPlugin({
            template: './index2.html',
            inject: 'body',
            filename: 'chanel2/index2.html',
            chunks: ['main2', 'lodash'],
            publicPath: 'http://www.a.com/'
        })
    ]

7. tree-shaking

7.1 tree-shaking

Tree Shaking 指的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,这就需要借助 webpack 里面自带的 Tree Shaking 这个功能来帮我们实现。它依赖于 ES2015 模块语法的静态结构特性,例如import和export。

现在写一个math.js

export const add = (x, y) => {
    return x + y
}

export const minus = (x, y) => {
    return x - y
}

和一个app.js

import { add, minus } from './math'
console.log(add(5, 6))

会发现app.js中没用到minus方法,然后我们来写webpack.config.js,引入一个optimization属性称为usedExports:

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode: 'development',

    entry: './src/app.js',

    plugins: [
        new HtmlWebpackPlugin()
    ],

    optimization: {
        // 该属性实现tree-shaking
        usedExports: true
    },
}

假设不加该属性,则add和没被使用的minus都会被暴露:

假如添加该属性,则只有add会被暴露。且minus被提示未被使用:

但是如果导入某个第三方库比如axios而不去使用,webpack还是会正常打包,无法识别到该库被导入后未被使用。

7.2 sideEffects

注意 Webpack 不能百分百安全地进行 tree-shaking。有些模块被引入后不希望进行tree-shaking,比如css模块和全局js配置模块。因为这些模块没有导出任何模块,那么可能会导致被webpack摇掉,导致bug。

可以在package.json中设置sideEffects的值:

  • true:所有文件都有副作用,都不能被tree-shaking
  • false:所有文件都没有副作用,都能被tree-shaking
  • 数组:数组中的文件不能被tree-shaking,别的都可以

比如现在我们不想css模块被摇掉,我们就可以写:

"sideEffects": [
    "*.css"
]

8. 常见问题

8.1 loader和plugin的概念和区别?

Loader本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作,运行在打包之前。
默认情况下,在加载模块的时候,webpack只支持对js文件打包。像遇到css、sass、png等这些类型的文件的时候,webpack则无能为力,这时候就需要配置对应的loader进行文件内容的解析。因此,loader运行在NodeJS中。仅仅只是为了打包。

常见配置方式:

module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
            },
            {
                test: /\.(csv|tsv)$/,
                use: 'csv-loader'
            },
            {
                test: /\.xml$/,
                use: 'xml-loader'
            },
        ]
    },

Plugin就是插件,可以扩展 Webpack 的功能。在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。plugin不仅只局限在打包,资源的加载上,它的功能要更加丰富。从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理各种各样的任务。

常见用法:

    plugins: [
        new HtmlWebpackPlugin({
            // html模板
            template: './index.html',
            // 输出文件名
            filename: 'app.html',
            // script标签位置(默认在head中)
            inject: 'body'
        }),
        new MiniCssExtractPlugin({
            filename: 'styles/[contenthash].css'
        })
    ],
    optimization: {
        minimizer: [
            new CssMinimizerPlugin()
        ]
    }

8.2 常见的loader和plugin有哪些?

loader

  • style-loader:将css添加到DOM的内联样式标签style里,然后通过 dom 操作去加载 css
  • css-loader/less-loader:前者允许导入css,后者允许导入less并将less转换为css
  • file-loader:分发文件到output目录并返回相对路径
  • html-minify-loader:压缩HTML
  • babel-loader:用babel来转换ES6到ES5
  • awesome-typescript-loader:将 TypeScript 转换成 JavaScript
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • cache-loader:可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

plugin

  • html-webpack-plugin:打包后自动创建html
  • web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
  • terser-webpack-plugin:压缩js,支持es6
  • css-minimizer-webpack-plugin:压缩css
  • compression-webpack-plugin:压缩文件
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载
  • serviceworker-webpack-plugin:为网页应用增加离线缓存功能
  • clean-webpack-plugin: 目录清理

8.3 五个核心概念

1、Entry(入口):指示webpack以哪个文件为入口起点打包。分析构建内部依赖图。

2、Output(出口):指示webpack打包后的资源bundles输出到哪里去,以及如何命名。

3、Loader:Loader让webpack能够去处理那些非javaScript文件【图片文件、html文件、样式文件】(webpack自身只理解javascript)

4、Plugins(插件):可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。

5、Mode(模式):指示webpack使用相应模式的配置。

8.4 webpack常见性能优化

(1) 缩小loader打包范围

通过include和exclude来缩小loader的处理范围

        module.exports = {
            mode: 'development',
            mdule: {
                rules: [
                    {
                        test: /\.js$/,
                        include: /\.\src/,//应用于src目录下
                        exclude: /node_modules/, //排除某些文件或目录
                        use: ["xx-loader"]
                    }
                ],
            }
        }

(2) 缓存loader结果
当文件内容不变时,经过相同的loader解析后,解析结果并没有改变,所以这个时候将loader的解析结果保存下来,让后续的解析直接使用保存的结果:

        module.exports = {
            module: {
                rules: [
                    {
                        test: /\.js$/,
                        use: [{
                            loader: 'cache-loader',
                            cacheDirectory: './cache'
                        }, ...loaders]
                    }
                ]
            }
        }

(3) 多线程打包
通过thread-loader开启一个线程池,后续会把loader放进线程池的线程中运行以提高运行效率。

        module.exports = {
            module: {
                rules: [
                    {
                        test: /\.js$/,
                        use: [{
                            loader: 'cache-loader',
                            cacheDirectory: './cache'
                        }, 'thread-loader']
                    }
                ]
            }
        }

(4) 代码分离
默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度。

将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件。

这个在之前笔记里写了

(5) 缓存第三方库
笔记写了

(6) tree shaking
笔记写了

(7) 懒加载
笔记写了

(8) 代码压缩 分离CSS 等等...

  • terser-webpack-plugin:压缩js,支持es6
  • css-minimizer-webpack-plugin:压缩css
  • compression-webpack-plugin:压缩文件
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载

8.5 webpack HMR

启动Webpack Dev Server:
开发者启动Webpack Dev Server,该服务器集成了webpack-dev-middleware和webpack-hot-middleware等中间件,用于处理静态文件服务和热更新逻辑。
在启动命令中,通常会包含--hot或hot: true等选项来启用HMR功能。

监听文件变化:
Webpack Dev Server通过其内部机制监听项目文件的变化。这种监听是独立于Webpack的watch mode的,因此即使Webpack没有处于watch模式,WDS也能正常工作。

重新编译:
当检测到文件变化时,Webpack Dev Server会触发Webpack重新编译这些变化的文件。编译结果通常不会直接写入磁盘,而是保存在内存中,以减少磁盘I/O操作。

建立WebSocket连接:
Webpack Dev Server与浏览器之间通过WebSocket建立了一个长连接。这个连接用于实时传递编译过程中的状态信息和模块更新信息。

推送更新信息:
Webpack Dev Server通过WebSocket将新模块的hash值等信息推送给浏览器。这些信息包括哪些模块发生了变化,以及这些模块的更新内容。

浏览器端处理:
浏览器端接收到WebSocket传递的信息后,会解析出需要更新的模块信息。
浏览器通过Ajax请求获取包含更新模块信息的JSON文件(manifest文件),该文件列出了所有需要更新的模块及其hash值。
接着,浏览器再通过JSONP请求获取具体的模块更新代码(update chunk)。

模块热替换:
浏览器端的HotModuleReplacement.runtime(HMR运行时)接收到更新代码后,会对比新旧模块的hash值,决定是否进行替换。
如果决定替换,HMR运行时会检查模块间的依赖关系,并更新模块及其依赖引用。