webpack工程化实战

720 阅读6分钟

项目准备

完整配置代码已上传至github,仅供参考:github.com/TimorCookie…

初始化

# 初始化 npm 配置文件
npm init -y 
# 安装核心库和命令行工具
npm install webpack@4.44.0 webpack-cli@3.3.12 --save-dev 

.npmrc

在为了解决npm龟速下载的糟糕体验时,我们一般会将npm源设置为淘宝镜像源

npm config set registry https://registry.npm.taobao.org

但是大家想想,万一某个同学克隆了你的项目之后,准备在他本地开发的时候,并没有设置淘宝镜像 源,又要人家去手动设置一遍,我们作为项目的发起者,就先给别人省下这份时间吧,只需要在根目录 添加一个 .npmrc 并做简单的配置即可:

# 根目录下创建 .npmrc 文件
touch .npmrc

# 在该文件内输入配置
registry=https://registry.npm.taobao.org/

创建webpack配置文件

# webpack.config.js
const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist')
  },
  mode: 'development'
}

样式处理

集成 css 样式处理

  • 安装 (style-loader 需要安装2.x版本)
npm install css-loader style-loader@2 --save-dev
  • 配置
# webpack.config.js
const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist')
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}

集成 less / sass

  • 安装 sass 和 less 相关模块( less-loader 安装7.x版本,sass-loader 安装10.x版本)

    #less
    npm install less less-loader@7 --save-dev
    
    #sass
    npm install node-sass sass-loader@10 --save-dev
    
  • 配置

    # webpack.config.js
    const path = require('path')
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, './dist')
      },
      mode: 'development',
      module: {
        rules: [
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
          },
          {
            test:/\.less$/,
            use: ["style-loader", "css-loader", "less-loader"]
          },
          {
            test:/\.scss/,
            use: ["style-loader", "css-loader", "sass-loader"]
          }
        ]
      }
    }
    

集成 postcss

Github:github.com/postcss/pos…

postcss之于css 相当于babel之于js

postcss主要功能只有两个:

  1. 把css解析成JS可以操作的抽象语法树AST,

  2. 第二就是调用插 件来处理AST并得到结果;

所以postcss 一般都是通过插件来处理css,并不会直接处理 比如:autoprefixer(自动补⻬浏览器前缀) cssnano(css压缩)

  • 安装(postcss-loader 需要安装6.x版本)

    npm install postcss-loader@6 autoprefixer cssnano --save-dev
    
  • 配置postcss.config.js

    # postcss.config.js
    module.exports = {
      plugins: [require('autoprefixer'), require('cssnano')]
    }
    
  • 配置浏览器兼容版本

    # 配置package.json
    "browserslist":["last 2 versions", "> 1%"],
      
    # 或者直接在postcss.config.js里配置 module.exports = {
      plugins: [
        require("autoprefixer")({
          overrideBrowserslist: ["last 2 versions", "> 1%"],
        }),
    ], };
    
    # 或者创建.browserslistrc文件 
    > 1%
    last 2 versions
    not ie <= 8
     
    
  • 配置webpack.config.js

    # webpack.config.js
    const path = require('path')
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, './dist')
      },
      mode: 'development',
      module: {
        rules: [
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader', 'postcss-loader']
          },
          {
            test:/\.less$/,
            use: ["style-loader", "css-loader", "postcss-loader"]
          },
          {
            test:/\.scss/,
            use: ["style-loader", "css-loader", "postcss-loader"]
          }
        ]
      }
    }
    

样式分离

经过如上几个loader处理,css最终是打包在js中的,运行时会动态插入head中,但是我们一般在生产环 境会把css文件分离出来(有利于用户端缓存、并行加载及减小js包的大小),这时候就用到 mini-css- extract-plugin 插件。

  • 安装(需要安装1.x版本)

    npm install mini-css-extract-plugin@1 -D
    
  • 使用

    # webpack.config.js
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, './dist')
      },
      mode: 'development',
      module: {
        rules: [
          // MiniCssExtractPlugin 需要参与模块解析,须在此设置此项,不再需要style-loader
          {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
          },
          {
            test:/\.less$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader, 'postcss-loader']
          },
          {
            test:/\.scss/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
          }
        ]
      },
      plugins: [new MiniCssExtractPlugin({
        filename: [name].css,
        ...
      })]
    }
    

图片 / 字体文件处理

url-loaderfile-loader 都可以用来处理本地的资源文件,如图片、字体、音视频等。功能也是 类似的, 不过url-loader 可以指定在文件大小小于指定的限制时,返回 DataURL ,不会输出真实的 文件,可以减少昂贵的网络请求。

  • 安装(使用url-loader 必须要 安装 file-loader)

    npm install url-loader file-loader --save-dev
    
  • 使用

    # webpack.config.js
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, './dist')
      },
      mode: 'development',
      module: {
        rules: [
          // MiniCssExtractPlugin 需要参与模块解析,须在此设置此项,不再需要style-loader
          {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
          },
          {
            test:/\.less$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader, 'postcss-loader']
          },
          {
            test:/\.scss/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
          },
          {
    				test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
            use: [
              {
                // 仅配置 url-loader 就行,内部会自动调用 file-loader
                loader: 'url-loader', 
                options: {
                  limit: 10240,
                  name: '[name]_[hash:6].[ext]',
                  outputPath: 'assets' // 设置资源输出目录
                }
              }
            ]
          }
        ]
      },
      plugins: [new MiniCssExtractPlugin({
        filename: [name].css,
        ...
      })]
    }
    

    注意:

    limit的设置要设置合理,太大会导致JS文件加载变慢,需要兼顾加载速度和网络请求次数。 如果需要使用图片压缩功能,可以使用 image-webpack-loader

    webpack文件指纹策略:

    • hash策略 是以项目为单位的,项目内容改变,则会生成新的hash,内容不变则hash不变

    • chunkhash 以chunk为单位,当一个文件内容改变,则整个chunk组的模块hash都会改变

    • contenthash 以自身内容为单位

HMTL 页面处理

htmlwebpackplugin会在打包结束后,自动生成一个 html 文件,并把打包生成的 js 模块引入到该 html 中。

  • 安装

    npm i html-webpack-plugin@4 --save-dev
    
  • 参数列表

     
    title: 用来生成⻚面的 title 元素
    filename: 输出的 HTML 文件名,默认是 index.html, 也可以直接配置带有子目录。
    template: 模板文件路径,支持加载器,比如 html!./index.html
    inject: true | 'head' | 'body' | false ,注入所有的资源到特定的 template 或者 templateContent 中,如果设置为 true 或者 body,所有的 javascript 资源将被放置到 body 元素的底部,'head' 将放置到 head 元素中。
    favicon: 添加特定的 favicon 路径到输出的 HTML 文件中。
    minify: {} | false , 传递 html-minifier 选项给 minify 输出
    hash: true | false, 如果为 true, 将添加一个唯一的 webpack 编译 hash 到所有包含的脚本和 CSS 文件,对于解除 cache 很有用。
    cache: true | false,如果为 true, 这是默认值,仅仅在文件修改之后才会发布文件。 showErrors: true | false, 如果为 true, 这是默认值,错误信息会写入到 HTML ⻚面中 chunks: 允许只添加某些块 (比如,仅仅 unit test 块)
    chunksSortMode: 允许控制块在添加到⻚面之前的排序方式,支持的值:'none' | 'default' | {function}-default:'auto'
    excludeChunks: 允许跳过某些块,(比如,跳过单元测试的块)
     
    
  • 配置

    # webpack.config.js 
    const path = require("path");
    const htmlWebpackPlugin = require("html-webpack-plugin");
    module.exports = {
     ...
      plugins: [
        new htmlWebpackPlugin({
          title: "My App",
          filename: "app.html",
          template: "./src/index.html"
    }) ]
    };
    

打包文件清理

clean-webpack-plugin这个插件是用来帮我们清除打包之后 dist 目录下的其他多余或者无用的代码,因为我们之前可能生成过其他的代码,如果不清楚的话可能多个代码掺杂在一起容易把我们搞混乱了clean-webpack-plugin 插件 就是这样由来的。每次生成代码之前先将 dist 目录清空

  • 安装

    npm install clean-webpack-plugin --save-dev
    
  • 使用

    # webpack.config.js 
    const path = require("path");
    const htmlWebpackPlugin = require("html-webpack-plugin");
    const const { CleanWebpackPlugin } = require("clean-webpack-plugin")
    module.exports = {
     ...
      plugins: [
        new htmlWebpackPlugin({
          title: "My App",
          filename: "app.html",
          template: "./src/index.html"
    		}),
        new CleanWebpackPlugin({})
      ]
    };
    

    Q:clean-webpack-plugin:如何做到dist目录下某个文件或目录不被清空?

    A:使用配置 项:cleanOnceBeforeBuildPatterns 案例:cleanOnceBeforeBuildPatterns: ["/*", "!dll", "!dll/"], !感 叹号相当于exclude 排除,意思是清空操作排除dll目录,和dll目录下所有文件。 注意:数组列表里的 “**/*”是默认值,不可忽略,否则不做清空操作。

sourceMap

源代码与打包后的代码的映射关系,通过sourceMap定位到源代码。 在dev模式中,默认开启,关闭的话 可以在配置文件里配置devtool:"none"

devtool的介绍:webpack.js.org/configurati…

  • eval:速度最快,使用eval包裹模块代码,
  • source-map:产生 .map 文件
  • cheap:较快,不包含列信息
  • Module:第三方模块,包含 loader 的 sourcemap (比如jsx to jsbabel 的 sourcemap)
  • inline: 将.map作为DataURI嵌入,不单独生成 .map 文件

配置推荐

 devtool:"cheap-module-eval-source-map",// 开发环境配置
//线上不推荐开启
devtool:"cheap-module-source-map", // 线上生成配置
 

配置开发服务器

每次改完代码都需要重新打包一次,打开浏览器,刷新一次,很麻烦,我们可以安装使用 webpackdevserver来改善这块的体验

  • 安装

    npm install webpack-dev-server@3.11.0 --save-dev
    
  • 配置

    • package.json
    
    {
      ...
      "script": {
        ...
        "server": "webpack-dev-server"
      }
    }
    
    • webpack.config.js
    # webpack.config.js
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const htmlWebpackPlugin = require("html-webpack-plugin");
    const const { CleanWebpackPlugin } = require("clean-webpack-plugin")
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, './dist')
      },
      mode: 'development',
      devServer: {
        contentBase: "./dist",
        open: true,
        port: 8080
      }
      module: {
        rules: [
          // MiniCssExtractPlugin 需要参与模块解析,须在此设置此项,不再需要style-loader
          {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
          },
          {
            test:/\.less$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader, 'postcss-loader']
          },
          {
            test:/\.scss/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
          },
          {
    				test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
            use: [
              {
                // 仅配置 url-loader 就行,内部会自动调用 file-loader
                loader: 'url-loader', 
                options: {
                  limit: 10240,
                  name: '[name]_[hash:6].[ext]',
                  outputPath: 'assets' // 设置资源输出目录
                }
              }
            ]
          }
        ]
      },
      plugins: [
        new MiniCssExtractPlugin({
          filename: [name].css,
          ...
      	}),
        new htmlWebpackPlugin({
          title: "My App",
          filename: "app.html",
          template: "./src/index.html"
    		}),
        new CleanWebpackPlugin({})
      ]
    }
    
    • 启动

      npm run serve
      

      启动服务后,会发现dist目录没有了,这是因为devServer把打包后的模块不会放在dist目录下,而是放 到内存中,从而提升速度

解决跨域

联调期间,前后端分离,直接获取数据会跨域,上线后我们使用nginx转发,开发期间,webpack就可以搞定这件事

启动一个服务器,mock一个接口npm i express -D

// 创建一个server.js 修改scripts "server":"node server.js"
# server.js
const express = require('express')
const app = express()
app.get('/api/info', (req,res)=>{
  res.json({
    name:'Timokie',
    age:18, 
    msg:'hello, Timokie!'
  }) 
})
app.listen('9092')
// node server.js
// http://localhost:9092/api/info

项目中安装axios工具npm i axios -D

# index.js
import axios from 'axios'
axios.get('http://localhost:9092/api/info').then(res=>{
    console.log(res)
})

产生跨域问题,此时我们可以修改 webpack.config.js 来设置服务器代理

module.exports={
  ...
	proxy: {
    "/api": {
      target: "http://localhost:9092"
    }
	}
}

修改index.js请求路径

 axios.get("/api/info").then(res => {
  console.log(res);
});

模块热替换

Hot Module Replacement(**HMR:**模块热替换)

模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

启动hmr

# webpack.config.js
module.exports = {
  ...
 
  devServer: {
    contentBase: "./dist",
    open: true,
    hot:true, //即便HMR不生效,浏览器也不自动刷新,就开启hotOnly hotOnly:true
  },
  
  ...
}

配置文件头部引入webpack

# webpack.config.js
//const path = require("path");
// const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//const HtmlWebpackPlugin = require("html-webpack-plugin");
//const CleanWebpackPlugin = require("clean-webpack-plugin");
const webpack = require("webpack");

在插件配置处添加:

# webpack.config.js
module.exports = {
  ...,
   
	plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: "src/index.html"
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
}

案例

  • 处理 css 模块 HMR

     
    # index.js
    import "./css/index.css";
    var btn = document.createElement("button"); 
    btn.innerHTML = "新增"; 
    document.body.appendChild(btn);
    btn.onclick = function() {
      var div = document.createElement("div");
      div.innerHTML = "item";
      document.body.appendChild(div);
    };
    
    # index.css
    div:nth-of-type(odd) {
      background: yellow;
    }
    
  • 处理 js 模块 HMR

    需要使用module.hot.accept来观察模块变更从而更新

    # counter.js
    function counter() {
      var div = document.createElement("div");
      div.setAttribute("id", "counter");
      div.innerHTML = 1;
      div.onclick = function() {
        div.innerHTML = parseInt(div.innerHTML, 10) + 1;
      };
      document.body.appendChild(div);
    }
    export default counter;
    
    
    # number.js
    function number() {
      var div = document.createElement("div");
      div.setAttribute("id", "number");
      div.innerHTML = 13000;
      document.body.appendChild(div);
    }
    export default number;
    
    
    # index.js
    import counter from "./counter";
    import number from "./number";
    counter();
    number();
    if (module.hot) {
      module.hot.accept("./b", function() {
        document.body.removeChild(document.getElementById("number"));
        number();
      });
    }
    

集成 Babel 处理 ES6

官方网站:babeljs.io/

中文网站:www.babeljs.cn/

Babel是 JavaScript 编译器,能将 ES6 代码转换成 ES5 代码,让我们开发过程中放心使用JS新特性而不用担心兼容性问题,并且还可以通过插件机制根据需求灵活的扩展。

Babel在执行编译的过程中,会从项目根目录下的.babelrc 的 JSON 文件中读取配置。没有该文件会从 loader 的 options 地方读取配置。

  • 安装 babel

    npm install @babel/core @babel/preset --save-dev
    

    babel-loader是 webpack 与 babel 的通信桥梁,不会做把es6转成es5的工作,这部分工作需要用到@babel/preset-env来做

    • es6+ ----->babel(presets-env)-----> es5

    • flow语法 ---->babel(presets-flow)->es5

    • jsx语法 ---->babel(preset-react) ->es5

    • ts语法 ---->babel(preset-ts) -->es5

  • 配置 webpack.config.js

    # webpack.config.js
    module.exports = {
      ...
      module: {
        rules: [  
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: "babel-loader",
              options: {
                presets: ["@babel/preset-env"]
              }
           }}
         ]
       }
    }
    

    通过上面的几步 还不够,默认的 Babel 只支持 let 等一些基础的特性转换,Promise 等一些还有转换过 来,这时候需要借助@babel/polyfill,把 es 的新特性都装进来,来弥补低版本浏览器中缺失的特性

  • 安装生产依赖 @babel/polyfill

    # 注意: @babel/polyfill 是生产依赖
    npm install @babel-polyfill --save
    
  • 修改入口文件,在顶部注入 polyfill

    # index.js
    import "@babel/polyfill"
    

执行打包命令后,会发现打包的体积大了很多,这是因为polyfill默认会把所有特性注入进来,假如我想我用到的es6+,才 会注入,没用到的不注入,从而减少打包的体积,就需要配置按需加载,减少冗余

# webpack.config.js
# webpack.config.js
module.exports = {
  ...
  module: {
    rules: [  
      {
        test: /\.js$/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: [
                [
                  "@babel/preset-env",
                  {
                    targets: {
                      edge: "17",
                      firefox: "60",
                      chrome: "67",
                      safari: "11.1",
                    },
                    //新版本需要指定核心库版本
                    corejs: 2,
                    useBuiltIns: "entry",
                  },
                ],
              ],
            },
          },
        ],
      },
     ]
   }
}

useBuiltIns 选项是 babel 7 的新功能,这个选项告诉 babel 如何配置 @babel/polyfill 。

它有三 个参数可以使用:

  • entry:需要在 webpack 的入口文件里 import "@babel/polyfill" 一次。 babel 会根据你的使用情况导入垫片,没有使用的功能不会被导入相应的垫片。
  • usage:不需要 import ,全 自动检测,但是要安装 @babel/polyfill 。
  • false:如果你 import "@babel/polyfill" ,它不会排 除掉没有使用的垫片,程序体积会庞大。(不推荐)

扩展: babelrc文件: 新建.babelrc文件,把options部分移入到该文件中,就可以了

 
#.babelrc
 {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          edge: "17",
          firefox: "60",
          chrome: "67",
          safari: "11.1",
        },
        //新版本需要指定核心库版本
        corejs: 2,
        useBuiltIns: "entry",
      },
    ],
  ],
},