Webpack 上 -04

147 阅读6分钟

webpack

核心概念

一切静态资源视为模块,又叫静态资源模块打包器。

通过入口文件递归构建依赖图,构建抽象语法树(如没有引用的东西就不会被找到),借助不同 loader 处理相应的文件源码,最终输出目标环境(浏览器)可执行的代码。

通常使用其构建项目时,维护的是一份配置文件(webpack.config),如果整个 webpack 视为一个函数,那么这份配置就是函数的参数,通过修改参数来控制输出的结果。

借助于 loader/plugin 可以差异化处理不同的文件类型。如有个性化需求,还可以实现自定的 loader/plugin

webpack 从零到一

  1. 初始化目录
yarn init
  1. 安装webpack webpack-cli
yarn add webpack webpack-cli -D

  1. 创建相关文件 index.html index.js
<!-- index.html -->
<html>
  <head>
    <meta charset="utf-8">
    <title>webpack</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="text/javascript" src="./output/main.js"></script>
<!-- // output/main.js是打包后的js代码路径 -->
</html>
// index.js
function test(content) {
  document.querySelector('#app').innerHTML = content;
}

test('something');

在不关联的情况下通过 webpack 打包:配置package.json

"scripts": {
    "build": "webpack ./src/index.js -o ./output --mode=development --devtool=cheap-module-source-map"
  },

其中:

  webpack: 使用  
  ./src/index.js: 读取 index 文件
  -o ./output: 输出目录为output
  --mode=development: 防止压缩,启用开发环境
  --devtool=cheap-module-source-map: 与源码进行关联(默认eval()源码为字符串)参考下面 source-map 干嘛地

产出文件: output main.js 期望产出文件打印 #app 的content

产物分析:外层加了闭包,防止变量被外部访问

/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
function test(content) {
  document.querySelector('#app').innerHTML = content;
}

test('something');
/******/ })()
;
//# sourceMappingURL=main.js.map

面试题:loader 和 plugins 有什么区别?

  1. loader 针对不同的文件类型进行处理,局限于一个类型的文件,写的时候需要匹配文件后缀。
  2. plugins 对于webpack 整个实例周期都生效,更新、打包都不分文件类型。它对 webpack 的钩子起作用,比如编译的时候或者编译完成的时候执行。

webpack 从一到二

  1. 新建 webpack.config.js

    const path = require('path');
    
    module.exports = {
      mode: 'development',
      devtool: 'cheap-module-source-map',
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'output'),
        filename: 'main.js' // 默认名字 main.js
      },
    }
    // 对比 build 命令
    // ./src/index.js -o ./output --mode=development --devtool=cheap-module-source-map"
    

    此时,直接在package.json 中 build 后面的代码相当于写在 config.js 中了

    "scripts": {
        "build": "webpack"
      },
    

    source-map 干什么用的?

    使用 source-map index.js打断点,main.js 给出源码 image.png 默认使用 eval image.png 源码与产物的映射关系,有助于打断点找错误

  2. 增加es6的转换能力 添加 src/es6.js

    export default class CountChange {
      count = 1
    
      increment = () => {
        this.count++
      }
    
      decrease = () => {
        this.count--;
      }
    }
    // index.js 中引入
    

    index.js 做一些修改

    const instance = new CountChange();
    
    function test(content) {
      document.querySelector('#app').innerHTML = content;
    }
    
    test(instance.count)
    

    产物分析:并没有进行 es 降级

    __webpack_require__.r(__webpack_exports__);
    /* harmony export */ __webpack_require__.d(__webpack_exports__, {
    /* harmony export */   "default": () => (/* binding */ CountChange)
    /* harmony export */ });
    class CountChange {
      count = 1
    
      increment () {
        this.count++
      }
    
      decrease () {
        this.count--;
      }
    }
    

    安装babel!!!

    yarn add @babel/core @babel/preset-env babel-loader -D
    

    增加配置

    module: {
      rules: [{
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env']
            ],
          }
        }
      }]
    }
    

    再次打包,产物

    var CountChange = /*#__PURE__*/_createClass(function CountChange() {
      var _this = this;
    
      _classCallCheck(this, CountChange);
    
      _defineProperty(this, "count", 1);
    
      _defineProperty(this, "increment", function () {
        _this.count++;
      });
    
      _defineProperty(this, "decrease", function () {
        _this.count--;
      });
    });
    
  3. 增加装饰器 装饰器:类方法其增强作用的高阶函数,不修改类本身

    // es6.js
    const decorator = (target, key, descriptor) => {
      target[key] = function (...args) {
        console.log(this.count);
        return descriptor.value.apply(this, args);
      };
      return target[key];
    }
    
    export default class CountChange {
      count = 1
    
      @decorator
      ...
    }
    

    此时编译会报错,因为@不识别

    安装相应插件:

    yarn add @babel/plugin-proposal-decorators -D
    

    加上相应的plugin

    //config.js
    preset: [...],
    plugins: [
      ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ]
    

    产物分析:

    (_descriptor = _applyDecoratedDescriptor(_class.prototype, "increment", [decorator], {
      configurable: true,
      enumerable: true,
      writable: true,
    

    验证装饰器:

    // index.js
    setInterval(() => {
      instance.increment()
      test(instance.count)
    }, 1000)
    
  4. 用于生产的react脚手架 react 在函数口是render函数

    index.js
    //render(<App />, $el)
    

    render 来自react-dom 包,并且需要编译

    安装

    yarn add react react-dom @babel/preset-react -S
    

    配置

    // config.js
    presets: [
        '@babel/preset-env',
        '@babel/preset-react'
      ],
    

    -D 和 -S 区别: -D 依赖都在开发环境下,打包的时候不回出现在产物里 -S 线上和开发都会有,打包之后会出现在产物里

    修改index.js

    import React from 'react';
    import { render } from 'react-dom';
    
    const App = () => <div>App</div>;
    
    render(<App />, document.querySelector('#app'));
    

    产物分析:

    var App = function App() {
      return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement("div", null, "App");
    };
    
    (0,react_dom__WEBPACK_IMPORTED_MODULE_1__.render)( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(App, null), document.querySelector('#app'));
    })();
    
    /******/ })()
    ;
    //# sourceMappingURL=main.js.map
    
  5. 缓存包提取

    // config.js
    module: {...},
    optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              filename: 'vendor.js',
              chunks: 'all', // async 打异步的包,initial 同步的包,不区分 为 all
              test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/
            }, // 两个包领出来,不要出现在main中
          }
        }
      }
    

    产物分析,多出一个vendor 其实就是领出来的东西,这里是react-dom 和 react

    记得在index.html 中引入 vendor!!

    问题vendor 是干嘛的?

    在开发的时候某些依赖组是不变的,发布之后代码对于用户而言不用每次都更新,直接一个强缓存,用户下载一次就不需要再下载了 因此在开发的时候将这部分代码剔除出去

    问题:vendor 过大了怎么办?

    可以使用 externals 相当于远程使用 cdn

      output: {...},
      externals: {
        'react': 'React' // 相当于 import React from 'react' => const React = window.React => src="react.cdn"
      }
    
  6. css loader

    1. 添加一个css文件
      #app {
        color: red;
      }
    

    直接打包会报错

      ERROR in ./src/style.css 1:0
    Module parse failed: Unexpected token
    
    1. 安装 css loader
      yarn add style-loader css-loader -D
    
    1. 配置,加一个rules
    // config.js
    rules: [{...},
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader']
    }]
    

    css-loader 是处理文件的 style-loader 是向页面注入 style 标签的

  7. 样式抽离 样式插件

    1. 安装
    yarn add mini-css-extract-plugin -D
    
    1. 导入! 插件的特点是必须导入,loader 不用导入
    // config.js
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    
    1. 配置
    // config.js
    module: {...},
    plugins: [
      new MiniCssExtractPlugin({
        filename: "[name].css",
        chunkFilename: "[id].css"
      })
    ],
    

    需要切换style-loader,因为style-loader 是将样式作为style标签注入页面的,现在不需要了 image.png

    // config.js
    rules: [{...},
    {
      test: /\.css$/,
      use: [MiniCssExtractPlugin.loader, 'css-loader']
    }]
    

    产出了样式表,但是没有引入。又需要手动去引入,显然,一次次手动不符合开发情景

  8. 自动引入脚本和样式表

    yarn add html-webpack-plugin -D
    

    安装 --> 引入 --> 配置

    // config.js
    plugins: [
      new HtmlWebpackPlugin({
        template: './index.html'
      }),
      new MiniCssExtractPlugin({...})
    

    处理图片/自定义文件方法相同

  9. 热更新 HMR -- 类似ajax 异步更新 目前为止,我们的操作为:打包+刷新 模式查看代码效果。可以借助本地开发服务器来解决这个问题

    1. 安装
    yarn add webpack-dev-server -D
    
    1. package.json 中添加命令:
    // package.json
    "scripts": {
      "build": "webpack",
      "start": "webpack serve"
    },
    

    开发模式的包在内存中,不输出文件 memory.fs

    目前修改 App 组件,页面实时更新了,只不过还是【刷新】

    修改一下 devServer 配置,顺便改一下端口

    //webpack.config.js
    entries: ...,
    devSever: {
      port: 8000,
      hot: true
    },
    

    还是在【刷新】,更新需要注册一个回调:开启热更新会监听根组件

    //index.js
    if (module.hot) {
      module.hot.accept(App, () => {
        render(<App />, document.querySelector('#app'));
      });
    }
    

    或者可以写成import 形式

    // index.js
    import App from './App'
    if (module.hot) {
      module.hot.accept('./App', () => { // 第一个参数为路径
        render(<App />, document.querySelector('#app'));
      });
    }
    

    根节点只要有更新了,就会执行回调 产出:页面保存后不会出现刷新页面的情况了,直接改变

异步组件打包和代码热更新

  1. 异步组件 react lazy
//index.js
const lazy = fn => class extends React.Component {
  state = {
    Component: () => null
  }
  // 默认加载的时候为一个 loading 图片

  async componentDidMount() {
    const { default: Component } = await fn();
    this.setState({ Component });
  }
  // 异步加载数据
  
  render() {
    const Component = this.state.Component;
    return <Component {...this.props} />;
  }
  // 返回真是的组件
}

// 声明一个异步组件,用lazy 包裹导入组件
const Async = lazy(() => import('./Async'));

// 指定产出的模块名称(注意这里的注释是有用的):
const Async = lazy(() => import(/* webpackChunkName: "Async" */ './Async'));

写出Async 组件

// src/Async
import React from 'react'

export default function Async() {
  return (
    <div>
      Async
    </div>
  );
}

导入后放在App 后面

// index.js
const App = () => <div>App <Async /></div>;

报错:Uncaught ReferenceError: regeneratorRuntime is not defined

原因:babel默认只转换新的JavaScript语法(syntax),如箭头函数等,而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,此时需要一些辅助函数(babel 6.x以下版本借助polyfill,需要在entry之前或根文件头部引入,本课程均以babel 7之后的标准讲解)

现在解决 直接安装regeneratorRuntime

yarn add @babel/plugin-transform-runtime -D

配置:

// config.js
// 在babel 的 plugins里
plugins: [
  "@babel/plugin-transform-runtime",
  ["@babel/plugin-proposal-decorators", { "legacy": true }],
]

output 加hash

output: {
    path: path.resolve(__dirname, 'output'),
    filename: '[name].[hash:6].js' // 默认名字 main.js
  },

xxxxx require.ensure 异步路由 webpack 支持的书写形式

  getComponent () {
    require.ensure(_, () => {
      const ensure = require('./requireEnsure').default;
      cb(ensure);
    }, err, 'Home')
  }

import 和 require 的区别

  • import 静态导入语法:必须在顶级作用域中写,不能在条件中使用,在代码执行前,就是知道所有依赖内容
  • require 可以在函数或者事件内部写,因此执行中会出现问题

webpack 插件与loader 书写