自动清理构建目录产物

每次构建的时候不清理目录,造成构建的输出目录output文件越来越多

通过npm script清理构建目录

rm -rf ./dist && webpack
rimraf ./dist && webpack

自动清理构建目录

通过clean-webpack-plugin
默认删除outpput指定的输出目录

  1. plugins: [
  2. new CleanWebpackPlugin()
  3. ]

PostCSS插件autoprefixer自动补齐CSS3前缀

IE(Trident)- -ms
火狐(Geko)- -moz
谷歌(Webkit)- -webkit
欧朋(Presto)- -o

  1. module: {
  2. rules: [
  3. {
  4. test: /.less$/,
  5. use: [
  6. MiniCssExtractPlugin.loader,
  7. 'css-loader',
  8. 'less-loader',
  9. {
  10. loader: 'postcss-loader',
  11. options: {
  12. plugins: () => {
  13. require('autoprefixer')({browser: ['last 2 version', '>1%', 'ios7']})
  14. }
  15. }
  16. }
  17. ]
  18. }
  19. ]
  20. }

移动端CSS px自动转换成rem

以前:css媒体查询-缺点:需要写多套适配样式代码

rem是什么

W3C对rem的定义:font-size of the root element
rem和px的对比
rem是相对单位
px是绝对单位

px自动转换rem

使用px2rem-loader
页面渲染时计算根元素的font-size值

  • 可以使用手淘的lib-flexible库
  • https://github.com/amfe/lib-flexible

    1. module: {
    2. rules: [
    3. {
    4. test: /.less$/,
    5. use: [
    6. 'css-loader',
    7. 'less-loader',
    8. {
    9. loader: 'px2rem-loader',
    10. options: {
    11. remUnit: 75, //1个rem相对于75像素,对应750的视觉设计稿
    12. remPrecision: 8 //小数点后8位
    13. }
    14. }
    15. ]
    16. }
    17. ]
    18. }

    由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport来替代此方。

    静态资源内联

    含义

    指css、js内联到html中,图片、字体联结到代码中

    意义

    代码层面

  • 页面框架初始化层面

  • 上报打点
  • css内联避免页面闪动

网络请求层面

  • 小图片或者字体内联

    HTML和JS内联

    HTML使用raw-loader

    1. ${require('raw-loader!./meta.html')}

    JS内联

    1. <script>${require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')}</script>

    CSS内联

  • 方案- 借助style-loader

    module: {
      rules: [
          {
        test: /.less$/,
        use: [
          {
            loader: 'style-loader',
            options: {
              insertAt: 'top', //样式插入到<head>
              singleton: tue //将所有style标签合并成一个
            }
          },
          'css-loader',
          'less-loader'
        ]
      }
    ]
    }
    
  • 方案二 html-inline-css-wbepack-plugin

    多页面应用打包

    多页面应用(MPA)概念

    每一次页面跳转的时候,后台服务器都会给返回一个新的html文档。这种类型的网站就是多页网站,也是多页应用。

    多页面打包基本思路

    每个页面对应一个entry,一个html-webpack-plugin
    缺点:每次新增删除页面都要修改webpack配置

    entry: {
    index: './src/index.js',
    search: './src/search.js'
    },
    plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src/index.html'),
      filename: 'index.html',
      chunks: ['index'],
      inject: true,
      minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
      }
    }),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src/search.html'),
      filename: 'search.html',
      chunks: ['search'],
      inject: true,
      minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
      }
    })  
    ]
    

    多页面打包通用方案

    动态获取entry和设置html-webpack-plugin数量
    利用glob.sync

    entry: global.sync(path.join(_dirname,'./src/*/index.js'))
    

    source map

    使用source map

    作用:通过source map定位到源代码

  • sourcemap科普文:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html

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

  • 线上排查问题的时候可以将sourcemap上传到错误监控系统

    source map关键字

    | eval | 使用eval包裹模块代码 | | —- | —- | | source map | 产生.map文件 | | cheap | 不包含列信息 | | inline | 将.map作为DataURL嵌入,不单独产生.map文件 | | module | 包含loader的source map |

source map类型

截屏2022-02-11 下午4.54.38.png

提取页面公共资源

基础库分离

思路:将react、react-dom等基础包通过cdn引入,不打入bundle中
方法:

1.使用html-webpack-externals-plugin

截屏2022-02-11 下午6.44.24.png

2.使用SpiltChunkPlugin

webpack4内置的,代替CommonsChunkPlugin插件
chunks参数说明:

  • async:异步引入的库进行分离(默认)
  • initial:同步引入碓库进行分离
  • all:所有引入的库进行分离(推荐)

    module.exports = { 
    optimization: { 
      splitChunks: {
        chunks: 'async',
        minSize: 30000,
        maxSize: 0,
        minChunks: 1, 
        maxAsyncRequests: 5, 
        maxInitialRequests: 3, 
        automaticNameDelimiter: '~', 
        name: true,
        cacheGroups: { 
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10 
          }
        } 
      }
      } 
    };
    

    test参数说明:匹配出需要分离的包

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

    minChunks参数说明:设置最小饮用次数
    minSize参数说明:设置分离包的体积大小

    module.exports = { 
    optimization: {
          splitChunks: { 
        minSize: 0, 
        cacheGroups: {
                  commons: {
                      name: 'commons', 
            chunks: 'all', 
            minChunks: 2
            }
          } 
        }
      } 
    };
    

    TreeShaking的使用和原理分析

    概念:

    1 个模块可能有多个方法,只要其中的某个方法使⽤到了,则整个文件都会被打到 bundle ⾥面去,tree shaking 就是只把用到的方法打入 bundle ,没⽤到的⽅法会在 uglify 阶段被擦除掉。

    使用

    webpack4 默认⽀支持,在 .babelrc ⾥设置 modules: false 即可

  • production mode的情况下默认开启

要求:必须是 ES6 的语法,CJS 的⽅式不⽀持

原理

DCE (Dead code elimination)

代码不会被执行,不可到达
代码执行的结果不会被⽤到
代码只会影响死变量(只写不读)

ES6 模块的特点

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable的

    代码擦除

    uglify 阶段删除⽆⽤代码

    Scope Hoisting的使用和原理分析

    未使用:构建后的代码存在大量闭包代码

    会导致什什么问题?

    ⼤量作⽤域包裹代码,导致体积增大(模块越多越明显)
    运行代码时创建的函数作用域变多,内存开销变大

    模块转换分析截屏2022-02-12 上午11.20.32.png

    结论:

  • 被 webpack 转换后的模块会带上一层包裹

  • import 会被转换成 __webpack_require

    进⼀步分析 webpack 的模块机制截屏2022-02-12 上午11.22.35.png

    分析:

  • 打包出来的是一个 IIFE (匿名闭包)

  • modules 是一个数组,每一项是一个模块初始化函数
  • __webpack_require ⽤来加载模块,返回 module.exports
  • 通过 WEBPACK_REQUIRE_METHOD(0) 启动程序

    scope hoisting 原理截屏2022-02-12 上午11.24.01.png

    原理:将所有模块的代码按照引用顺序放在⼀个函数作用域⾥,然后适当的重命名一 些变量以防止变量名冲突
    对比: 通过 scope hoisting 可以减少函数声明代码和内存开销
    scope hoisting 使⽤
    webpack mode 为 production 默认开启
    必须是 ES6 语法,CJS 不支持 ```javascript module.exports = { entry: {
      app: './src/app.js',
      search: './src/search.js' 
    
    }, output: {
      filename: '[name][chunkhash:8].js', 
    
    path: __dirname + ‘/dist’ }, plugins: [
  • new webpack.optimize.ModuleConcatenationPlugin() ] }; ```

    代码分割和动态import

    代码分割的意义

    对于⼤的 Web 应⽤用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被使用到。webpack 有一个功能就是将你的代码库分割成 chunks(语块),当代码运⾏到需要它们的时候再进⾏加载。
    适⽤用的场景:
  • 抽离相同代码到一个共享块
  • 脚本懒加载,使得初始下载的代码更小

    懒加载 JS 脚本的方式

    CommonJS:require.ensure
    ES6:动态 import(目前还没有原⽣生⽀支持,需要 babel 转换)

    如何使用动态 import?

    安装 babel 插件
    npm install @babel/plugin-syntax-dynamic-import —save-dev
    ES6:动态 import(⽬前还没有原⽣生⽀支持,需要 babel 转换)

    //.babelrc中增加plugins
    "plugins": [
          "@babel/plugin-syntax-dynamic-import"
    ]
    

    效果
    动态加载的文件单独打包

    在webpack中使用Eslint

    制定团队的 ESLint 规范原则

  • 不重复造轮子,基于 eslint:recommend 配置并改进

  • 能够帮助发现代码错误的规则,全部开启
  • 帮助保持团队的代码⻛风格统一,而不是限制开发体验

截屏2022-02-12 下午7.46.37.png

ESLint 如何执⾏行行落地

  • 和 CI/CD 系统集成
  • 和 webpack 集成

    webpack 与 CI/CD 集成

截屏2022-02-12 下午7.47.59.png
本地开发阶段增加 precommit 钩⼦
安装 husky
npm install husky —save-dev
增加 npm script,通过 lint-staged 增量量检查修改的⽂文件

"scripts": {
    "precommit": "lint-staged"
},
"lint-staged": { 
     "linters": {
        "*.{js,scss}": ["eslint --fix", "git add"] 
  }
},

webpack 与 ESLint 集成
使⽤用 eslint-loader,构建时检查 JS 规范
.webpack.js的配置

module.exports = { 
  module: {
        rules: [{
      test: /\.js$/,
      exclude: /node_modules/, 
          use: [
          "babel-loader", 
        "eslint-loader”
      ] 
    }]
  }
};

.eslintrc的配置

module.exports = {
    "parser": "babel-eslint",
    "extends": "airbnb",
    "env": {
        "browser": true,
        "node": true
    },
    "rules": {
        "indent": ["error", 4]
    }
};

webpack打包组件和基础库

webpack 除了了可以⽤用来打包应⽤用,也可以⽤用来打包 js 库

实现⼀个⼤整数加法库的打包

  • 需要打包压缩版和非压缩版本
  • ⽀持 AMD/CJS/ESM 模块引⼊

    库的目录结构和打包要求

    打包输出的库名称:

  • 未压缩版 large-number.js

  • 压缩版 large-number.min.js

目录

|- /dist
|-     large-number.js
|-     large-number.min.js
|- webpack.config.js 
|- package.json
|- index.js
|- /src
|-     index.js

⽀持的使用方式

  1. 支持 ES module

    import * as largeNumber from 'large-number'; 
    // ...
    largeNumber.add('999', '1');
    
  2. 支持 CJS

    const largeNumbers = require('large-number'); 
    // ...
    largeNumber.add('999', '1');
    
  3. 支持 AMD

    require(['large-number'], function (large-number) { 
    // ...
     largeNumber.add('999', '1'); 
    });
    
  4. 可以直接通过 script 引⼊入

    <!doctype html> 
    <html>
    ...
    <script src="https://unpkg.com/large-number"></script> <script>
     // ...
     // Global variable 
     largeNumber.add('999', '1');
     // Property in the window object 
     window. largeNumber.add('999', '1'); 
     // ...
    </script> 
    </html>
    

    如何将库暴暴露露出去?

    设置output

  • library: 指定库的全局变量量
  • libraryTarget: ⽀支持库引⼊入的⽅方式
    //.webpack.config.js配置
    module.exports = { 
    mode: "production", 
    entry: {
      "large-number": "./src/index.js",
      "large-number.min": "./src/index.js" 
    },
      output: {
          filename: "[name].js", 
        library: "largeNumber", 
        libraryExport: "default", 
        libraryTarget: "umd"
      } 
    };
    

    如何指对 .min 压缩

    1.通过 include 设置只压缩 min.js 结尾的文件

    module.exports = { 
    mode: "none", 
    entry: {
      "large-number": "./src/index.js",
      "large-number.min": "./src/index.js"
    },
    output: {
      filename: "[name].js", 
      library: "largeNumber", 
      libraryTarget: "umd"
    }, 
    optimization: {
      minimize: true, 
      minimizer: [
        new TerserPlugin({
            include: /\.min\.js$/, 
        }),
      ],
    }
    };
    

    2.设置⼊口⽂件

    package.json 的 main 字段为 index.js
    //index.js
    if (process.env.NODE_ENV === "production") { 
    module.exports = require("./dist/large-number.min.js");
    } else {
      module.exports = require("./dist/large-number.js"); 
    }
    

代码位置:https://gitee.com/geektime-geekbang/geektime-webpack-course/tree/master/code/chapter03/large-number

webpack实现SSR打包

服务端渲染 (SSR) 是什么?

渲染: HTML+CSS+JS+Data->渲染后的HTML
服务端:

  • 所有模板等资源都存储在服务端
  • 内⽹机器器拉取数据更快
  • 一个 HTML 返回所有数据

    浏览器和服务器交互流程

    截屏2022-02-15 下午8.00.17.png

    客户端渲染 vs 服务端渲染

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

总结:服务端渲染 (SSR) 的核心是减少请求

SSR 的优势

减少⽩屏时间
对于SEO友好

SSR 代码实现思路

服务端

  • 使⽤ react-dom/server 的 renderToString 方法将 React 组件渲染成字符串
  • 服务端路由返回对应的模板

客户端

  • 打包出针对服务端的组件

    webpack ssr 打包存在的问题

    浏览器的全局变量 (Node.js 中没有 document, window)

  • 组件适配:将不兼容的组件根据打包环境进⾏适配

  • 请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios

    样式问题 (Node.js ⽆无法解析 css)

  • ⽅案一:服务端打包通过 ignore-loader 忽略略掉 CSS 的解析

  • 方案二:将 style-loader 替换成 isomorphic-style-loader

    如何解决样式不显示的问题?

  • 使⽤打包出来的浏览器端 html 为模板

  • 设置占位符,动态插⼊组件

截屏2022-02-17 下午8.28.46.png

⾸屏数据如何处理?

  • 服务端获取数据
  • 替换占位符

截屏2022-02-17 下午8.31.09.png

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

当前构建时的⽇志显示

展示⼀大堆日志,很多并不需要开发者关注

统计信息 stats

开发环境-在devServer的stats中设置以下内容
生产环境-直接设置stats
截屏2022-02-17 下午8.32.45.png
如何优化命令⾏的构建日志
使⽤用 friendly-errors-webpack-plugin

  • success: 构建成功的⽇志提示
  • warning: 构建警告的⽇志提示
  • error: 构建报错的日志提示

stats 设置成 errors-only

module.exports = { 
  entry: {
        app: './src/app.js',
        search: './src/search.js' 
  },
    output: {
        filename: '[name][chunkhash:8].js', 
    path: __dirname + '/dist'
    },
    plugins: [
+ new FriendlyErrorsWebpackPlugin()
    ],
+ stats: 'errors-only' 
};

构建异常和中断处理

如何判断构建是否成功?

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

构建异常和中断处理

webpack4 之前的版本构建失败不不会抛出错误码 (error code)

  • 0表示成功,非0失败

Node.js 中的 process.exit 规范
· 0 表示成功完成,回调函数中,err 为 null
· ⾮ 0 表示执行失败,回调函数中,err 不为 null,err.code 就是传给 exit 的数字
如何主动捕获并处理构建错误?
compiler 在每次构建结束后会触发 done 这 个 hook
process.exit 主动处理构建报错

plugins: [ 
  function() {
        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); 
        }
    }) 
  }
]