webpack4从零开始构建一

49次阅读

共计 12336 个字符,预计需要花费 31 分钟才能阅读完成。

前言

之前一段时间工作原因把精力都放在小程序上, 趁现在有点空闲时间, 刚好官方文档也补充完整了, 我准备重温一下 webpack 之路了, 因为官方文档已经写得非常详细, 我会大量引用原文描述, 主要重点放在怎么从零构建 webpack4 代码上, 这不是一个系统的教程, 而是从零摸索一步步搭建起来的笔记, 所以前期可能 bug 会后续发现继续修复而不是修改文章.

当前文章完整配置 webpack4_demo

PS:
2018/12/12 修改细节布局
2018/12/19 新增包管理工具介绍
2018/12/26 上传, 代码同步到第四篇文章
2019/03/14 上传, 补充代码到第一篇文章,status1 分支可见

淘宝 NPM 镜像

因为 npm 包是从国外服务器下载插件, 速度慢, 容易失败, 我们平时使用可以使用淘宝团队的替代镜像.

npm install -g cnpm --registry=https://registry.npm.taobao.org

registry.npm.taobao.org 安装所有模块. 当安装的时候发现安装的模块还没有同步过来, 淘宝 NPM 会自动在后台进行同步, 并且会让你从官方 NPM registry.npmjs.org 进行安装. 下次你再安装这个模块的时候, 就会直接从 淘宝 NPM 安装了.

淘宝 NPM 镜像

Yarn 包管理

Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。同时利用并行下载以最大化资源利用率,因此安装速度更快。

Node 版本支持: ^4.8.0 || ^5.7.0 || ^6.2.2 || >=8.0.0

官网安装

初始化一个新项目

yarn init

添加依赖包

yarn add [package]
yarn add [package]@[version]
yarn add [package]@[tag]

将依赖项添加到不同依赖项类别中, 分别添加到 devDependencies、peerDependenciesoptionalDependencies 类别中:

yarn add [package] --dev
yarn add [package] --peer
yarn add [package] --optional

升级依赖包

yarn upgrade [package]
yarn upgrade [package]@[version]
yarn upgrade [package]@[tag]

移除依赖包

yarn remove [package]

安装项目的全部依赖

yarn

Node 版本管理

我们可以安装一个版本管理器, 根据需求切换.
nvm-windows

安装版本

nvm install <version> [arch]

移除版本

nvm uninstall <version>

查看已安装版本

nvm ls

切换版本

nvm use [version] [arch]

概念

本质上,webpack 是一个现代 JavaScript 应用程序的 静态模块打包器 (static module bundler)。在 webpack 处理应用程序时,它会在内部创建一个 依赖图(dependency graph),用于映射到项目需要的每个模块,然后将所有这些依赖生成到一个或多个 bundle。

核心:

  1. 入口(entry)
  2. 输出(output)
  3. 加载器(loader)
  4. 插件(plugins)

安装

因为最高版本 nodejs 有点问题, 所以建议用 @9.0, 当然用 10 也是可以的, 只是每次都会警告太烦了.
安装依赖你们也可能选择其他安装方式

yarn add webpack

webpack4+ 版本还需要安装

yarn add webpack-cli

完成之后打开目录下的 package.json 可以看到当前依赖

{
  "dependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2"
  },
  "name": "webpack_demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

准备工作

根目录新增 index.html

<!doctype html>
<html>

<head>
    <title>webpack</title>
</head>

<body>
    <p>hello world!</p>
    <script src="index.js"></script>
</body>

</html>

创建 src 目录, 新增 style.scssindex.js 备用

html {
  background-color: #666;
  p {color: red;}
}
console.log('onload');

正常打开 index.html 没毛病

起步

根目录新增webpack.config.js 文件

const path = require("path");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 输出
  output: {
    // 打包文件名
    filename: "main.js",
    // 输出路径
    path: path.resolve(__dirname, "dist"),
    // 资源请求路径
    publicPath: '/',
  }
};

index.html 的 js 引用路径替换成输出路径

<script src="src/index.js"></script>
--------------------------------------
<script src="dist/main.js"></script>

执行命令

npx webpack --config webpack.config.js

打包完成之后直接打开 index.html 看, 没毛病.

npx

npx 是 npm@5.2.0+后出现的工具, 总的来说用法有几个

直接执行命令

一般我们会在 package.json 配置一些命令然后执行, 例如

npm run xxx

但是我们可以直接用 npx 运行

npx xxx

临时安装调用

npx create-react-app my-cool-new-app

执行这行命令 npx 会自动查找当前依赖包中的可执行文件,如果找不到,就会去 PATH 里找。如果依然找不到,就会帮你安装!帮你下载并且执行 create-react-app, 完了以后还不会留下痕迹

因为我们已经安装过 webpack 了, 这么长的命令还是直接在 package.json 配置调用吧

mode

刚才打包的时候如果在终端看到这段提示

WARNING in configuration
The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.
You can also set it to ‘none’ to disable any default behavior. Learn more: https://webpack.js.org/concep…

这是因为 webpack4 增加的一个配置项设定运行环境, 通过将 mode 参数设置为 development, production 或 none,可以启用对应环境下 webpack 内置的优化。默认值为 production。业务代码中可以通过 process.env.NODE_ENV 获取到当前环境模式做些不同处理

直接在 webpack.config.js 加上解决警告

mode: 'development'

index.js 改成

console.log(process.env.NODE_ENV);

重新打包查看效果

development 和 production 相同与区别

common

// parent chunk 中解决了的 chunk 会被删除
optimization.removeAvailableModules: true

// 删除空的 chunks
optimization.removeEmptyChunks: true

// 合并重复的 chunk
optimization.mergeDuplicateChunks: true

development

// 调试
devtool: eval

// 缓存模块, 避免在未更改时重建它们。cache: true

// 缓存已解决的依赖项, 避免重新解析它们。module.unsafeCache: true

// 在 bundle 中引入「所包含模块信息」的相关注释
output.pathinfo: true

// 在可能的情况下确定每个模块的导出, 被用于其他优化或代码生成。optimization.providedExports: true

// 找到 chunk 中共享的模块, 取出来生成单独的 chunk
optimization.splitChunks: true

// 为 webpack 运行时代码创建单独的 chunk
optimization.runtimeChunk: true

// 编译错误时不写入到输出
optimization.noEmitOnErrors: true

// 给模块有意义的名称代替 ids
optimization.namedModules: true

// 给模 chunk 有意义的名称代替 ids
optimization.namedChunks: true

production

// 性能相关配置
performance: {hints: "error"....}

// 某些 chunk 的子 chunk 已一种方式被确定和标记, 这些子 chunks 在加载更大的块时不必加载
optimization.flagIncludedChunks: true

// 给经常使用的 ids 更短的值
optimization.occurrenceOrder: true

// 确定每个模块下被使用的导出
optimization.usedExports: true

// 识别 package.json or rules sideEffects 标志
optimization.sideEffects: true

// 尝试查找模块图中可以安全连接到单个模块中的段。- -
optimization.concatenateModules: true

// 使用 uglify-js 压缩代码
optimization.minimize: true

loader

因为 webpack 用于编译 JavaScript 模块, 样式不在它的能力范围内, 所以我们需要引入 loader 做处理. 它支持引入任何其他类型的文件使用.

执行命令安装依赖

yarn add style-loader css-loader sass-loader node-sass

修改一下 webpack.config.js

const path = require("path");

module.exports = {
  // 入口
  entry: "./src/index.js",

  // 输出
  output: {
    // 打包文件名
    filename: "main.js",
    // 输出路径
    path: path.resolve(__dirname, "dist"),
    // 资源请求路径
    publicPath: '/',
  },

  module: {
    rules: [
      {test: /\.(css|scss)$/, // 匹配文件
        use: [
          "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面
          "css-loader", // 加载.css 文件将其转换为 JS 模块
          "sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
        ]
      }
    ]
  }
};

注意一下引用顺序, 处理是从右到左的, 所以上面会先编译样式转成模块再注入.

index.js 引入样式

import "./style.scss";

package.json 配置执行命令

"dev": "webpack"

执行命令

npm run dev

执行完看 dist 文件夹还是只有 main.js, 刷新页面看到样式已经被内嵌入页面的 head 位置了.

还有其他的一些文件如图片, 字体, 文档等官方文档很详细了, 这里略过, 直接安装依赖

yarn add file-loader xml-loader html-loader

图片处理

当你们在 js 引入图片的时候, 该图像将被处理并添加到 output 目录,并且变量将包含该图像在处理后的最终 url, 例如

import img from "./1.jpg";
---------------------------------------
32bbef3c0cb97aa96aaa0a07f3bfc5e4.jpg

同样标签中的图片和样式中的图片也会分别使用 html-loader/css-loaderj 进行类似的处理

接下来修改 webpack.config.js

const path = require("path");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 输出
  output: {
    // 文件名
    filename: "main.js",
    // 输出路径
    path: path.resolve(__dirname, "dist"),
    // 资源请求路径
    publicPath: '/',
  },

  module: {
    rules: [
      {
        test: /\.scss$/, // 匹配文件
        use: [
          "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面
          "css-loader", // 加载.css 文件将其转换为 JS 模块
          "sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
        ]
      },
      {test: /\.(png|svg|jpg|gif)$/, // 图片处理
        use: ["file-loader"]
      },
      {test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
        use: ["file-loader"]
      },
      {
        test: /\.xml$/, // 文件处理
        use: ["xml-loader"]
      },
      {
        test: /\.html$/, // 处理 html 资源如图片
        use: ["html-loader"]
      }
    ]
  }
};

管理输出

上面我们已经完成了基本的 webpack 使用, 接着随着项目增大我们可能会新增多个入口, 切割多份代码, 这时候继续写死输出资源名字就不合适了, 所以我们接下来会引入动态命名.

output.filename 有一些可用的模板

模板 描述
[hash:length(默认 20)] 模块标识符 (module identifier) 的 hash
[chunkhash:length(默认 20)]] chunk 内容的 hash
[name] 模块名称
[id] 模块标识符(module identifier)
[query] 模块的 query,例如,文件名 ? 后面的字符串

然后我们修改 webpack.config.js 输入文件名

filename: '[name].bundle.js'

到了这一步就已经能够输出文件了. 但是别急, 还有一个关键问题是我们怎么动态引入文件?

plugin

HtmlWebpackPlugin

HtmlWebpackPlugin 可以生成创建 html 入口文件, 动态引入编译后的外部资源.
执行命令安装依赖

yarn add html-webpack-plugin
plugins: [
  new HtmlWebpackPlugin({
    title: "test", // title
    template: "index.html" // 以 index 为模板
  })
]

运行命令后看到 dist 文件夹多了一个 index.html, 这就是我们动态创建的入口文件了, 直接打开看看效果.

clean-webpack-plugin

执行命令安装依赖

yarn add clean-webpack-plugin

这个插件可以帮你每次构建之前先删除一些没用的遗留文件, 推荐每次打包前先删除整个 dist 文件
接下来修改 webpack.config.js, plugin 内新增插件

new CleanWebpackPlugin()

webpack-dev-server

webpack-dev-server 为你提供了一个简单的 web 服务器,并且能够实时 [color=#2b91e3] 重新加载 (live reloading)
执行命令安装依赖

yarn add webpack-dev-server

然后我们修改 webpack.config.js 新增配置

devServer: {
    // 打开模式, Iframe mode 和 Inline mode 最后达到的效果都是一样的,都是监听文件的变化,然后再将编译后的文件推送到前端,完成页面的 reload 的
    inline: true,
    // 指定了服务器资源的根目录
    contentBase: path.join(__dirname, 'dist'),
    // 是否开启 gzip 压缩
    compress: false,
    port: 9000,
    // 是否开启热替换功能
    hot: true,
    // 是否自动打开页面, 可以传入指定浏览器名字打开
    open: true,
},

在 package.json 里新配置一条命令

"start": "webpack-dev-server"

运行命令之后会自动帮你从浏览器中启动页面 http://localhost:8080/, 后续一直监听代码变化, 改动之后会自动刷新页面看到效果.
注意的是 webpack-dev-server 输出的文件只存在于内存中, 不输出真实的文件

模块热替换(Hot Module Replacement 或 HMR)

刚才的 webpack-dev-server 配置虽然方便, 但是是属于整体重载, 很多时候我们只是修改一些小地方的话没必要这么耗费资源
模块热替换功能会在应用程序运行过程中替换、添加或删除模块,无需重新加载整个页面。

第一步,devServer 里 hot 替换成 hotOnly 配置
第二步, 引入 webpack,plugin 里加入

new webpack.HotModuleReplacementPlugin()

webpack.config.js 整体大概如此

const webpack = require('webpack');
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
  mode: "development",
  // 入口
  entry: "./src/index.js",
  // 输出
  output: {
    // 打包文件名
    filename: "[name].bundle.js",
    // 输出路径
    path: path.resolve(__dirname, "dist"),
    // 资源请求路径
    publicPath: '/',
  },
  devServer: {
    // 打开模式, Iframe mode 和 Inline mode 最后达到的效果都是一样的,都是监听文件的变化,然后再将编译后的文件推送到前端,完成页面的 reload 的
    inline: true,
    // 指定了服务器资源的根目录
    contentBase: path.join(__dirname, 'dist'),
    // 是否开启 gzip 压缩
    compress: false,
    port: 9000,
    // 是否开启热替换功能
    // hot: true,
    // 是否自动打开页面, 可以传入指定浏览器名字打开
    open: true,
    // 是否开启部分热替换功能
    hotOnly: true
  },
  module: {// 省略},
  plugins: [
    // 清除文件
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // title
      title: "test",
      // 模板
      template: "index.html"
    }),
    // 热替换模块
    new webpack.HotModuleReplacementPlugin()]
};

你们可以新建一个模块例如 test.js

export function log() {console.log("123");
}

然后 index.js 修改如下

import "./style.scss";
import {log} from "./test";
console.log(process.env.NODE_ENV);
if (module.hot) {module.hot.accept('./test', function () {console.log('Accepting the updated printMe module!');
    log();})
}

然后分别修改 index.js 和 test.js 看看控制台更新情况, 现在更新范围仅限 test.js 文件了

其他代码和框架

社区还有许多其他 loader 和示例,可以使 HMR 与各种框架和库 (library) 平滑地进行交互……

  • React Hot Loader:实时调整 react 组件。
  • Vue Loader:此 loader 支持用于 vue 组件的 HMR,提供开箱即用体验。
  • Elm Hot Loader:支持用于 Elm 程序语言的 HMR。
  • Redux HMR:无需 loader 或插件!只需对 main store 文件进行简单的修改。
  • Angular HMR:No loader necessary! A simple change to your main * NgModule file is all that’s required to have full control over the HMR APIs. 没有必要使用 loader!只需对主要的 NgModule 文件进行简单的修改,由 HMR API 完全控制。

tree shaking

webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony 模块)和未引用模块检测能力。新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json 的 “sideEffects” 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “ 纯净 ”,由此可以安全地删除文件中未使用的部分。

基于 ES6 的静态引用,tree shaking 通过扫描所有 ES6 的 export,找出被 import 的内容并添加到最终代码中。webpack 的实现是把所有 import 标记为有使用 / 无使用两种,在后续压缩时进行区别处理。

我们可以先看看效果, 在 test.js 下新增方法

export function dead() {console.log(321);
}

index.js 修改如下

import {
  log,
  dead
} from "./test";

log();

运行命令之后然后打开 dist 目录下的文件找到这段编译代码, 即使 index.js 引用了 dead 方法, 但是没有使用的话编译文件依然会删除

按理说应该会包含 dead 方法引入, 但是不知道是不是 webpack4 优化了这些步骤免去我们手动解决的烦恼, 后续再研究.

source map

应该都知道用来映射代码方便调试

devtool 构建速度 重新构建速度 生产环境 品质(quality)
(none) +++ +++ yes 打包后的代码
eval +++ +++ no 生成后的代码
cheap-eval-source-map + ++ no 转换过的代码(仅限行)
cheap-module-eval-source-map o ++ no 原始源代码(仅限行)
eval-source-map + no 原始源代码
cheap-source-map + o yes 转换过的代码(仅限行)
cheap-module-source-map o yes 原始源代码(仅限行)
inline-cheap-source-map + o no 转换过的代码(仅限行)
inline-cheap-module-source-map o no 原始源代码(仅限行)
source-map yes 原始源代码
inline-source-map no 原始源代码
hidden-source-map yes 原始源代码
nosources-source-map yes 无源代码内容

+++ 非常快速, ++ 快速, + 比较快, o 中等, – 比较慢, — 慢

配置环境

项目开发过程一般至少分开发环境和生产环境

开发环境 (development) 和生产环境 (production) 的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载 (live reloading) 或热模块替换 (hot module replacement) 能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

执行命令安装依赖 webpack-merge, 这个插件可以合并配置文件输出

yarn add webpack-merge

创建公用配置文件 webpack.common.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 输出
  output: {
    // 打包文件名
    filename: "[name].bundle.js",
    // 输出路径
    path: path.resolve(__dirname, "dist"),
    // 资源请求路径
    publicPath: ""
  },
  module: {
    rules: [
      {test: /\.(css|scss)$/, // 匹配文件
        use: [
          "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面
          "css-loader", // 加载.css 文件将其转换为 JS 模块
          "sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
        ]
      },
      {test: /\.(png|svg|jpg|jpeg|gif)$/, // 图片处理
        use: ["file-loader"]
      },
      {test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
        use: ["file-loader"]
      },
      {
        test: /\.xml$/, // 文件处理
        use: ["xml-loader"]
      },
      {
        test: /\.html$/, // 处理 html 资源如图片
        use: ["html-loader"]
      }
    ]
  },
  plugins: [
    // 清除文件
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // title
      title: "test",
      // 模板
      template: "index.html"
    })
  ]
};

创建生产配置文件 webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');


module.exports = merge(common, {
  mode: "development",
  // 原始源代码(仅限行)devtool: 'cheap-module-eval-source-map',
})

创建生产配置文件 webpack.prod.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: "production",
    // 原始源代码
    devtool: 'source-map',
})

创建开发环境文件 webpack.server.js

const webpack = require("webpack");
const path = require("path");
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: "development",
  // 原始源代码(仅限行)devtool: 'cheap-module-eval-source-map',
  devServer: {
    // 打开模式, Iframe mode 和 Inline mode 最后达到的效果都是一样的,都是监听文件的变化,然后再将编译后的文件推送到前端,完成页面的 reload 的
    inline: true,
    // 指定了服务器资源的根目录
    contentBase: path.join(__dirname, 'dist'),
    // 是否开启 gzip 压缩
    compress: false,
    port: 9000,
    // 是否开启热替换功能
    // hot: true,
    // 是否自动打开页面, 可以传入指定浏览器名字打开
    open: false,
    // 是否开启部分热替换功能
    hotOnly: true
  },
  plugins: [
    // 热替换模块
    new webpack.HotModuleReplacementPlugin()]
})

最后 package.json 命令配置修改如下:

"scripts": {
    "dev": "webpack --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack-dev-server --config webpack.server.js"
},
  "dependencies": {
    "clean-webpack-plugin": "^2.0.0",
    "css-loader": "^2.1.1",
    "file-loader": "^3.0.1",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.11.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.2.1",
    "webpack-merge": "^4.2.1",
    "xml-loader": "^1.2.1"
  },

第一阶段构建基本完成了, 然后我们可以再深度拓展一下

正文完
 0