webpack4 基础学习 一

232 阅读7分钟

引言

大概了解下,知道有哪些基础的概念先
完整配置篇webpack 基础配置

下面的这些内容呢,有点杂乱,看极客时间视频学习记录,踩个脚印,知道都有啥,各项深入学习留待后续。。。。

自动构建更新

在生产环境中不需要webpack的更新

自动构建更新的两种形态方式: watch配置 与  热更新配置

一、Webpack文件监听: 在发现文件资源源码发现变化时自动重新构建输出新的资源文件

开启监听方式 watch 配置两种:  

PS: 但该监听方式存在一个缺陷: 每次资源文件发生变化, webpack监听编译完成之后,需要开发者手动去刷新浏览器页面进行查看效果,监听生成的文件 放置于磁盘文件中

  1. 启动webpack命令时 带上—watch参数

scripts:.png

  1. 在配置webpack.config.js中 设置 watch:true

module. exports.png

Tips: watch监听原理解析 --- 轮询判断文件的最后编辑时间是否变化,当某文件发生变化,并不会立刻告诉监听着,而是先缓存起来,等待aggregateTimeout

wateh true,.png

二、热更新 image.png

热更新方式一: WDS

webpack-dev-server : 不输出文件,存放于内存中,不存于磁盘os中; 配合插件HotModuleReplacementPlugin(自动更新浏览器), 配置devServer

热更新方式二: WDM

Webpack-dev-middleware  需要引入node server,通常使用express 或者 koa 会将webpack输出的文件传输给到服务器 适用于较灵活的定制场景

Tips: 热更新原理实现 Webpack Dey Server.png

  1. webpack compiler 将JS编译成bundle
  2. HMR server 将热更新的文件输出给到HMR Runtime
  3. Bundle server 提供文件在浏览器的访问
  4. HMR Runtime 会被注入到浏览器中

启动阶段: 文件系统进行编译,将初始代码经过webpack comllier打包,传输给到bundle server,然后传递给到hmr runtime

更新阶段:  经过webpack compiler编译,hmr server传输给到hmr runtime (以json数据格式传输),hmr runtime 注入到浏览器中驱动更新且不用刷新页面

文件指纹策略

  • 概念: 文件名添加的hash字符串,即文件指纹
  • 作用: 版本管理,防止缓存,同时最大限度的利用缓存

文件指纹生成:

  1. Hash: 和整个项目的构建相关,只要项目文件出现修改,整个项目构建的hash就会改变,适用于图片,字体等资源文件
  2. chunkhash: 与webpack打包的chunk相关,不同的entry会生成不同的chunkhash值,一般用于js文件
  3. contenthash: 根据文件内容定义,文件内容不变则contenthash不变,  适用于css文件
module.exports = {
    mode: 'development',
    // ...
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name]_[hash:8].js' // js文件指纹设置,给定8位数hash值,name为占位符
    },
    // ...
}

代码压缩:

  1. Html压缩: 修改html-webpack-plugin,设置压缩参数
  2. Css压缩: 使用optimize-css-assets-webpack-plugin。同时使用cssnano(css预处理器);
  3. Js压缩: webpack4+中内置了uglifyjs-webpack-plugin,默认打包出来的js文件就是压缩的了,也可手动安装设置相关参数

Css压缩, 在webpack3之后可通过设置css-loader 参数进行压缩,但是css-loader在1.0版本之后移除了该参数,so,现在做css压缩无法通过css-loader参数设置进行压缩

  • optimize-css-assets-webpack-plugin
  • cssnano 预处理器

build: webpack —config webpack.prod.js。// 指定环境执行配置文件, 生产环境prod

dev: webpack-dev-server —config webpack.dev.js

自动清理构建目录

作用: 避免每次构建前手动删除dist文件夹
方法:
1. 使用依赖 clean-webpack-plugin(默认删除output指定输出目录)
2. 在命令行处添加rm -rf ./dist 语句 image.png

CSS3属性前缀自动补全

  1. 兼容不同浏览器内核
  2. 使用: postcss-loader插件autoprefixer CSS3样式使用 查阅网址: Can I use

移动端CSS px自动转换rem

  1. 引用loader: px2rem-loader
module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /.less$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader',
                    {
                        loader: 'px2rem-loader',
                        options: {
                            remUnit: 75,  // 适合750设计稿,
                            remPrecision: 8 // 转换为rem后小数点8位
                        }
                    }
                ]
            }
        ]
    }

}
  1. 在output html中内联前置引入lib-flexible脚本 手淘的lib-flexible库: 动态计算px转换为rem单位的值, 在页面打开时自动计算转换值

资源内联的意义

  • 代码层面:

    • 页面脚本的初始化脚本
    • 上报相关点 --- js/css初始化/加载完成节点的上报,
    • css内联避免页面闪动
  • 请求层面:减少http网络请求数量

    • 小图片或者字体内联(url-loader)

html内联,js内联 可借助raw-loader进行
css内联存在两种方式:
1. style-loader
2. html-inline-css-webpack-plugin插件

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- 使用了htmlwebpackplugin插件,该插件内置默认使用的ejs模版,以下 <%= ‘code’ %> 为ejs语法 -->
    <%= require('raw-loader!./meta.html') %>
    <title>Document</title>
    <script>
        <%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %>
    </script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

占位符

typedesc
[ext]资源后缀名
[name]文件名称
[path]相对路径
[folder]文件所在的文件夹
[contenthash]文件内容hash, 默认由md5生成
[hash]文件内容hash, 默认由md5生成
[chunkhash]构建生成的chunk文件hash, 默认由md5生成
[emoji]一个随机指代文件内容的emoji
example:   filename: '[name]_[hash:8].js'

devtool

延伸阅读:
一文看懂 webpack 的所有 source map
Webpack devtool source map

source-map

作用: 通过source map 定位到源文件

开发环境默认开启,线上环境关闭

线上环境开启会暴露源码

排查错误问题时可以降sourcemap上传到错误监控系统中

关键字 devtool: 'source-map', // eval, inline, source -map, inline-source -map .... 

  1. source map: 产生 .map文件, 单独列出文件,并在js文件中最后一行注明

dist.png

Pasted Graphic 3.png 在不使用source-map 配置的情况下打debugger断点调试时无法看到源码,只能看到压缩后的代码,难以进行定位

Pasted Graphic 4.png 使用source-map的情况下,可以很清楚的看到源码:

Pasted Graphic 5.png

  1. Cheap: 不包含列信息

  2. inline: 将.map作为dataURI嵌入,补单副产生map文件

  3. module: 包含loader的source map,在出错时可以定位错误信息

  4. Eval: 包裹模块代码

多页应用打包 MAP

  • 每一次页面跳转,服务器都会返回一个新的html文档,这种类型的网站即多页网站,也称多页应用。
  • 打包思路
    • 每一个页面对应一个entry,一个html-webpack-plugin
    • 缺点: 每次新增或删除页面,均需要修改webpack配置 约定:所有入口文件均放置于src文件中
      改进做法:动态获取entry和设置html-webpack-plugin数量 利用glob.sync
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const glob = require('glob');

// 约定: 所有入口文件均放置于src文件中, 在src文件夹下各自的文件夹中以index.js的形式存在
const setMPA = () => {
    const entry = {}, HtmlWebpackPlugins = [];
    const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
    // 当前的index不是item,是index
    Object.keys(entryFiles).map((index) => {
        const entryFile = entryFiles[index];
        const match = entryFile.match(/src\/(.*)\/index\.js/);
        const pageName = match && match[1];
        entry[pageName] = entryFile;

        HtmlWebpackPlugins.push(new HtmlWebpackPlugin({
            template: path.join(__dirname, `src/${pageName}/index.html`),
            filename: `${pageName}.html`,
            chunks: ['vendors', pageName], //利用了splitChunks动态添加抽离的chunk,vendors为占位符
        }))
    })
    return {
        entry,
        HtmlWebpackPlugins
    }
}

// 将entry和HtmlWebpackPlugins置于module.exports对象中的相应位置即可
const { entry, HtmlWebpackPlugins } = setMPA();

体积优化 ~~~ start

tree shaking 摇树优化

MDN概念:

Tree shaking 是一个通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code) 行为的术语。

它依赖于ES2015中的 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。

在现代 JavaScript 应用程序中,我们使用模块打包(如webpackRollup)将多个 JavaScript 文件打包为单个文件时自动删除未引用的代码。这对于准备预备发布代码的工作非常重要,这样可以使最终文件具有简洁的结构和最小化大小。

简化理解: tree shaking 就是将文件中需要用到的方法代码打入到bundle中,没有没用到的方法将在uglify阶段被擦除掉。

原理 & 注意事项

  1. 必须使用ES6语法,不支持CJS方式
  2. 使用时在.babelrc中设置modules: false或设置mode: none即可;production mode模式下默认开启
  3. 方法中存在副作用则tree shaking会失效,即该方法代码不会被擦除
  4. 理解DCE (Elimination)

Scope Hoisting

现象: 项目构建后的代码存在大量闭包代码
导致问题: 大量函数闭包包裹代码,导致bundle体积增大;运行代码时创建的函数作用域变多,内存开销变大

原理: 将所有模块代码按照引用顺序放在一个函数作用域中,然后适当的重命名变量以防止变量名冲突
对比: 通过scope hoisting 可以减少 函数声明代码量内存开销

了不起的 Webpack Scope Hoisting 学习指南

注意事项

  1. webpack3中需要添加代码:
  2. 必须使用ES6语法,不支持CJS方式
  3. 使用时设置mode: none即可;production mode模式下默认开启

代码分割, 动态import

代码分割的意义: 将代码库分割成chunks,当代码运行到需要他们的时候再进行加载

使用场景: 

  1. 抽离代码到相同的共享快中
  2. 脚本懒加载,使得出事下载的代码更小

懒加载JS脚本的方式

  1. commonjs: require.ensure

  2. ES6: 动态import (目前没有原生支持,需要babel支持转换): babel-plugin-syntax-dynamic-import

// 在 .babelrc 文件中的plugins中添加babel-plugin-syntax-dynamic-import,
// 在代码中使用的时候才import导入
function click() {
    // 这种import方式返回的是一个promise函数,需要在then中获取结果值
    import(‘../clickTimes.js’).then((res) => { console.log(res) }) 
}

webpack splitChunks

// 通过webpack4内置的splitchunks提取公共包
// 使用的时候需要将name: vendor放置到html模版chunks中,如下
module.exports = {

    // output: ...,
    // module: {},
    
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, `src/search/index.html`),
            filename: `search.html`,
            chunks: ['vendors', 'search'] // 'vendors' 为固定占位符
        })
    ],
    
    optimization: {
        splitChunks: {
            cacheGroups: {
                // minSize: 2, // 设置引用的chunk模块的大小
                deVendors: {
                    // test: /(react|react-dom)/,
                    // filename: '[name].js',
                    // 使用name字段时automaticNameDelimiter设置无效,展示为 deVendors.js
                    name: 'deVendors', 
                    chunks: 'all',
                    minChunks: 2, // 要求最少被引用次数为2
                    // 当使用filename 而非name字段时,可设置连接符号,如当前效果展示为 deVendors--home--personal.js
                    // automaticNameDelimiter: '--', 
                },
            },
        },
    },

}

公共资源包分离

  1. 外部CDN引入: 将不需要被修改的依赖进行外部CDN引入,降低bundle包的size

    • 利用插件HtmlWebpackExternalsPlugin
    module.exports = {
        entry: '',
        module: {},
        plugins: [
            new HtmlWebpackExternalsPlugin({
                externals: [
                    {
                        module: 'react',
                        entry: 'https://unpkg.com/react@17/umd/react.development.js',
                        global: 'React',
                    },
                    {
                        module: 'react-dom',
                        entry: 'https://unpkg.com/react-dom@17/umd/react-dom.development.js',
                        global: 'ReactDOM',
                    },
                ],
            })
        ],
    }
    
    • 直接externals引入
    module.exports = {
        entry: '',
        module: {},
        externals: ['react', 'react-dom'],
        plugins: [],
    }
    

    上述两种cdn引入,使用的时候都需要在对应的html文件中引入cdn script链接

  2. 利用splitChunks将react , react-dom等公共资源包单独打包

module.exports = {
    entry: '',
    //...
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    test: /(react|react-dom)/,
                    name: 'vendors',
                    chunks: 'all',
                }
            },
        },
    }
}

动态polyfill

图片压缩

体积优化 ~~~ end


ESlint的落地

  1. webpack 和CI/CD系统、集成

    • 本地开发阶段增加precommit钩子

      安装husky npm install husky —save-dev
      package.json文件中增加npm script,通过lint-staged增量检查修改的文件,如果代码中存在预发问题,就会阻止代码的commit提交

      // package.json
      {
          “scripts”: { "precommit": "lint-staged" }
      
          “Lint-staged”: {
              “linters”: {
                  “*.{js,css}: [ “eslint —fix”, “git add” ]
              }
          }
      }
      
    • 在CI/CD阶段增加一个lint pipline,发布代码的时候在build的时候 进行检查  (可以避免本地开发的时候绕过本地precommit的检查机制后没有进行检查的bug)

  2. webpack 和 ESlint。 (不适合老项目介入,默认检查所有的require文件)
    已被弃用: eslint-loader 、babel-eslint
    推荐使用: eslint-webpack-plugin @babel/eslint-parser

webpack打包库和组件

打包应用/js库

打包输出的库的名称
非压缩版: xxxx.js
压缩版:xxxx.min.js

暴露库: 
library 指定库的全局变量,打包出来的库名;
librarytarget 指定库的引入方式

demo: 发布一个npm包npm 发包遇到的问题

webpack实现ssr打包

客户端渲染服务端渲染
请求多个请求(HTML,数据等)1个请求
加载过程HTML&数据串行加载1个请求返回HTML&数据
渲染前端渲染服务端渲染
可交互图片等静态资源加载完成,js逻辑完成可交互图片等静态资源加载完成,js逻辑完成可交互

总结: 服务端渲染(SSR)的核心是减少请求 (减少白屏时间,对SEO友好)

实现思路:

  • 服务端:
    • 使用react-dom/server 的renderToString方法将React组件渲染成字符串
    • 由服务端路由返回对应的字符串模版
  • 客户端:
    • 打包出针对服务端的组件

webpack ssr打包存在的问题

  • 浏览器的全局变量(Node.js中没有document,window)
    • 组件适配: 将不兼容的组件根据不同的环境进行适配
    • 请求适配: 将fetch或者ajax发送请求的写法改成isomorphic-fetch 或者axios
  • 样式问题
    • 方案一: 服务端打包通过 ignore-loader忽略css的解析
    • 方案二: 将style-loader替换成isomorphic-style-loader

优化构建时命令行显示的log日志

侧重开发者关注点的输出日志

统计信息stats结合依赖 friendly-errors-webpack-plugin使用

module.exports = {
  //...
  stats: 'errors-only'
};
PresetAlternativeDescription
"errors-only"none只在发生错误时输出
"minimal"none只在发生错误或有新的编译时输出
"none"false没有输出
"normal"true标准输出
"verbose"none全部输出

webpack 构建异常 及其 中断处理

如何判断构建的成功与否
在CI/CD 的pipline 或者发布系统需要知道当前的构建状态
每次构建完成之后输入 echo $? 获取错误码

webpack4之前的版本构建失败不会抛出错误码
nodejs中的process.exit规范

如何主动捕获并处理构建错误

module.exports = {
    // ...
    plugins: [
        function () {
            // compiler 在每次构建结束后都会触发done这个hook
            this.hooks.done.tap('done', (stats) => {
                if (
                    stats.compilation.errors
                    && stats.compilation.errors.length
                    && process.argv.indexOf('--watch') === -1
                ) {
                    console.log('build error')
                    process.exit(1);// 主动处理构建报错
                }
            })
        }
    ],
}

编写可维护的webpack配置包

环境配置抽离成npm包的意义

  1. 通用性: 业务开发者无需关注构建配置,统一团队的构建脚本
  2. 可维护性: 构建配置合理的拆分,README文档、changelog文档等
  3. 质量: 冒烟测试、单元测试、测试覆盖率,持续集成

构建配置管理的可选方案

  1. 通过多个配置文件管理不同环境的构建,webpack --config 参数进行控制
  2. 将构建配置设计成一个库,比如: hjs-webpack、Nuutrino、webpack-blocks
  3. 抽成一个工具进行管理,比如: create-react-app、kyt、nwb等 将所有的配置放在一个文件中,通过--env参数控制分支进行选择

构建配置包的设计

  • 通过多个文件管理不同环境的webpack配置
  • 抽里成一个npm依赖包管理

image.png

通过webpack-merge合并基础配置和不同的环境配置

构建包的功能模块设计和目录结构设计

image.png

image.png

冒烟测试

  • 构建是否成功
  • 每次构建完成build目录是否有内容输出
    • 是否有JS、CSS资源文件
    • 是否有HTML文件

.....

单元测试 & 测试覆盖率

image.png 编写单元测试用例

  • 技术选型: Mocha + Chai
  • 测试代码: describe,it, except
  • 测试命令: mocha add.test.js 单元测试接入
  1. 安装mocha + chai: npm i mocha chai -D
  2. 新建test目录,并增加xxxx.test.js
  3. 在package.json中的scripts字段中增加test命令 "test": "node_modules/mocha/bin/mocha"
  4. 执行测试命令: npm run test

持续集成

  • 作用:
    • 快速发现错误
    • 防止分支的功能大幅偏离主干
  • 核心措施: 代码集成到主干志气,必须通过自动化测试,只要有一个测试失败,就不能集成

。。。。。。

Git commit规范 & changelog

webpack4+ 性能更加的原因

  1. V8带来的优化
    • for of替代forEach
    • MapSet替代Object
    • includes替代indexOf
  2. 默认使用更快的md4 hash算法
  3. webpacks AST可以直接从loader传递给AST,减少解析时间
  4. 使用字符串方法代替正则表达式

webpack 优化

分析构建速度 / 构建体积

  1. 初级分析: webpack内置的stats

  2. nodejs中使用stats 缺点: 上诉两点的颗粒度比较粗,看不出问题所在

  3. 速度分析: speed-measure-webpack-plugin

    • 每个loader和插件的耗时
    • 分析整个包的耗时
  4. 体积分析: webpack-bundle-analyzer: 构建完成后会在8888端口展示大小

构建速度优化

  1. 使用高版本的webpack 和 node.js (构建时间差异大)
  2. 多进程多实例构建: 资源并行解析可选方案 官方:webpack4内置thread-loader
    可选方案: HappyPack(可选中的主流^_^)、 parallel-webpack
多进程多实例并行压缩
  1. parallel-uglify-plugin 插件, 参考 深入浅出的webpack构建工具---ParallelUglifyPlugin优化压缩(十)
  2. uglifyjs-webpack-plugin 开启parallel参数
  3. terser-webpack-plugin 开启parallel参数
进一步分包,预编译资源模块
  1. 分包: 设置Externals
  • 将react/react-dom基础包通过cdn引入,不打入到bundle中
  • 使用html-webpack-plugin
  1. 分包: DLLPlugin
  • 使用DllReferencePlugin引用manifest.json
缓存提高二次构建速度

缓存思路

  • babel-loader 开启缓存(解析阶段)
  • terser-webpack-plugin 开启缓存 (代码压缩阶段)
  • 使用cache-loader或者hard-source-webpack-plugiin (模块转换阶段)
最小构建目标

目的: 尽可能的少构建模块,如babel-loader不解析node_modules文件

模块解析的过程中减少文件搜索范围,合理使用alias

  • 优化resolve.modules配置(减少模块搜索层级)
  • 优化resolve.mainFields配置
  • 优化resolve.extensions配置 (文件后缀配置)

构建体积优化

  1. 图片压缩
  • 基于Node库的imagemin 或者 tinypng API
  • 使用: 配置image-webpack-loader

imagemin优点分析 : 定制选项、可引入第三方优化插件(如pngquant)、可处理多种图片格式

Errors 集合

webpck4学习之新手错题集


神文参考:
webpack配置
由浅至深了解webpack异步加载背后的原理
webpack学习日志-12-文件指纹与代码拆分
webpack进阶篇(十七):静态资源内联