Webpack 模块化开发与配置

129 阅读10分钟

webpack 模块化的配置,包括处理 js转换 / css / html / eslint / 图片等的 loader、plugin 配置。 传说中前端配置工程师的基本修养……


一 模块(Module)

模块是为了完成某种功能所需的程序或子程序,模块是系统中职责单一可替换的部分。

模块化就是把系统代码分为一系列职责单一且可替换的模块。模块化开发是指如何开发新的模块和复用已有的模块来实现应用的功能。

javascript 的三大主流模块规范已在上篇介绍过,这么不多赘述。

Webpack 中一切皆模块,包括 javascript、css、html、图片、字体、富媒体及 less、sass、 各种 javascript 库等等,webpack 可以针对不同的模块用不同的 loader 做不同的解析编译,做到按需加载。比如对于 less 代码,可以先用 less-loader 转换为 css 代码,用 css-loader 处理其中的 url/import 等逻辑,最后用 style-loader 把 css 插入到 <style> 标签里。

1. Webpack对模块的增强:import() 和神奇注释(Magic Comments)

Webpack 导入模块的方式不限于三大主流模块规范,提供了 import() 引入模块的方式。

与 import from 不同的是, import from 是静态分析打包的,而 import() 是动态分析打包的,通过异步的方式加载模块,import() 返回一个 Promise 对象。并且 import() 被视为分割点,被请求的模块和它引用的所有子模块,打包后会分割到一个单独的 chunk 中。

// 想象我们有一个从 cookies 或其他存储中获取语言的方法
const language = detectVisitorLanguage();
import(`./locale/${language}.json`).then((module) => {
  // do something with the translations
});

神奇注释(Magic Comments),是指可以通过在 import() 中添加注释实现对打包的文件做一定的操作,比如给 chunk 命名、选择不同模式。

// 单个目标
import(
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  /* webpackExports: ["default", "named"] */
  'module'
);

// 多个可能的目标
import(
  /* webpackInclude: /\.json$/ */
  /* webpackExclude: /\.noimport\.json$/ */
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  /* webpackPrefetch: true */
  /* webpackPreload: true */
  `./locale/${language}`
);
Magic Comments说明
webpackChunkNamechunk 文件的名称
webpackInclude在导入解析过程中,用于匹配的正则表达式,只有匹配到的模块才会被打包
webpackExclude用于匹配的正则表达式,匹配到的模块不会被打包
webpackMode指定以不同的模式解析动态导入,可选值lazy/lazy-once/eager/weak
webpackPrefetch是否预取模块,及其优先级
webpackPreload是否预加载模块,及其优先级

通过神奇注释,import()不再是简单的 JavaScript 异步加载器,还是任意模块资源的加载器,举例说明:如果我们页面用到的图片都放在src/assets/img文件夹下,你们可以通过下面方式将用到的图片打包到一起:

import(/* webpackChunkName: "image", webpackInclude: /\.(png|jpg|gif)/ */ './assets/img');

2. Webpack对模块的增强:require

require.context()

require.context(directory, includeSubdirs, filter)可以批量将 directory内的文件全部引入进文件,并且返回一个具有resolve的 context 对象,使用context.resolve(moduleId)则返回对应的模块。该功能十分有用。

  • directory:目录 string;
  • includeSubdirs:是否包含子目录,可选,默认值是 true;
  • filter:过滤正则规则,可选项。

require.context() 会将所有的文件都引入进 bundle。

require.resolve()

require.resolve(dependency: String)可以获取模块的唯一 ID, 并把模块真实引入 bundle。

require.include()

require.include(dependency: String)引入一个不需要执行的 dependency,这样可以用于优化输出 chunk 中依赖模块的位置。

/**
 * 输出:
* entry chunk: file.js and a
* anonymous chunk: b
* anonymous chunk: c
* 不使用 require.include('a'),输出的两个匿名 chunk 都会有模块 a
*/
require.include('a');
require.ensure(['a', 'b'], function (require) {
  /* ... */
});
require.ensure(['a', 'c'], function (require) {
  /* ... */
});

二 使用 Babel 转换 js 代码

在 webpack 中编写 JavaScript 代码,可以使用最新的 ES 语法,而最终打包的时候,webpack 会借助 Babel 将 ES6+语法转换成在目标浏览器可执行 ES5 语法。

Babel 拥有自己的 cli,可以单独安装使用,具体参考 Babel 官网,这里不赘述。

在 npm 项目中,Babel 支持两种文件配置方式:

  • 在 package.json 中配置 babel 属性
  • 在项目根目录下单独创建 .babelrc 或者 .babelrc.js 文件
// package.json
{
    "name": "my-package",
    "version": "1.0.0",
    "babel": {
        "presets": ["@babel/preset-env"]
    }
}
// .babelrc
{
    "presets": ["@babel/preset-env"]
}

如果使用了webpack,也可以配置在 webpack 的配置文件中:

// 安装开发依赖
npm i webpack babel-loader webpack-cli @babel/core @babel/preset-env @babel/plugin-transform-runtime -D
// 将 runtime 作为依赖
npm i @babel/runtime -S

// 配置示例
module.exports = {
    entry: './babel.js',
    mode: 'development',
    devtool: false,
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                [
                                    '@babel/preset-env',
                                    {
                                        useBuiltIns: 'usage'
                                    }
                                ]
                            ]
                        }
                    }
                ]
            }
        ]
    }
};

1. env 选项

配置 env 选项可以实现在不同环境下使用不同的 Babel 配置。

env 选项的值将从 process.env.BABEL_ENV 获取,如果没有的话,则获取 process.env.NODE_ENV 的值,如果这个也没有,会默认设置为 "development"。

{
  "env": {
    "production": {
      "presets": ["@babel/preset-env"]
    }
  }
}

2. Polyfill

polyfill:在 JavaScript 中表示一些可以抹平浏览器实现差异的代码,比如某浏览器不支持 Promise,可以引入es6-promise-polyfill 等库来解决。

Babel 的插件分为两类:转换插件和语法解析插件。转换插件用于对语法进行转换,如 ES6 转换为 ES5,语法解析插件用来扩展语法,如解析 React 特殊设计的jsx语法。对于各种情况下的语法转换与解析,需要一个一个的引入相应的插件,这种方式比较麻烦,一般场景下,可以直接使用官方推出的插件组合@babel/preset-env

@babel/preset-env可以根据开发者的配置按需加载对应的插件,还可以根据代码执行平台环境和具体浏览器版本生成相应的 js 代码。

@babel/preset-env有两个重要的配置项:useBuiltInstarget

/**
 * useBuiltIns: usage|entry|false,一般采用 usage 这种方式,即指定按需加载。
 * target: 指定构建目标
 */
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage|entry|false"
      "targets": {
          // "browsers": "IE 10" // 指定IE 10浏览器
          "node": "8.9.3" // 如果在node环境中,指定node版本号
      }
    }]
  ]
}

Browserslist 目标浏览器的设置

项目的实际开发与运行中,很多情况下要做一定的浏览器兼容,而 Browserslist 就是用来设置目标浏览器的工具。许多开发工具包括 npm、babel 都兼容内置了 Browserslist。

Browserslist 的配置可以放在 package.json 中,也可以单独放在配置文件.browserslistrc 中。相关的开发工具都会主动查找 browserslist 的配置文件,根据 browserslist 配置找出对应的目标浏览器集合。

// package.json 内配置
{
    "browserslist": ["last 2 version", "> 1%", "maintained node versions", "not ie < 11"]
}
// 兼顾不同环境的配置,设置BROWSERSLIST_ENV 或者 NODE_ENV可以配置不同的环境变量
{
    "browserslist": {
        "production": ["> 1%", "ie 10"],
        "development": ["last 1 chrome version", "last 1 firefox version"]
    }
}
// 项目的根目录下.browerslistrc文件:
// 注释是这样写的,以#号开头
// 每行一个浏览器集合描述
last 2 version
> 1%
maintained node versions
not ie < 11

// 兼顾不同环境的配置
[production staging]
> 1%
ie 10

[development]
last 1 chrome version
last 1 firefox version

常见的浏览器集合范围说明:

范围说明
last 2 versionscaniuse.com网站跟踪的最新两个版本,假如 iOS 12 是最新版本,那么向后兼容两个版本就是 iOS 11 和 iOS 12
> 1%全球超过 1%人使用的浏览器,类似> 5% in US则指代美国 5%以上用户
cover 99.5%覆盖 99.5%主流浏览器
chrome > 50 ie 6-8指定某个浏览器版本范围
unreleased versions说有浏览器的 beta 版本
not ie < 11排除 ie11 以下版本不兼容
since 2013 last 2 years某时间范围发布的所有浏览器版本
maintained node versions所有被 node 基金会维护的 node 版本
current node当前环境的 node 版本
dead通过last 2 versions筛选的浏览器中,全球使用率低于0.5%且官方声明不在维护或者事实上已经两年没有再更新的版本
defaults默认配置,> 0.5% last 2 versions Firefox ESR not dead

浏览器名称列表(大小写不敏感):

浏览器说明浏览器说明
Chromechrome浏览器ChromeAndroid/and_chrchrome 安卓移动浏览器
EdgeEdge浏览器Explorer/ieie浏览器
Android安卓webview浏览器Baidu百度浏览器
Firefox/ff火狐浏览器FirefoxAndroid/and_ff火狐安卓浏览器
iOS/ios_safiOS Safari 浏览器ExplorerMobile/ie_mobie 移动浏览器
Safari桌面版本 SafariNodenodejs
Operaopera浏览器OperaMobile/op_mobopera 移动浏览器
OperaMini/op_minioperaMini 浏览器QQAndroid/and_qqQQ 安卓浏览器
Samsung三星浏览器UCAndroid/and_ucUC 安卓浏览器
ElectronElectronBlackBerry / bb黑莓浏览器

Babel Polyfill 的最佳实践:

useBuiltIns:'usage'可以近乎完美的解决我们的 Polyfill 问题,它是按需引入模块,根据 .browserslist + 业务实际代码来设置引入 Polyfill,不会多余的引入。

三 样式相关配置

Webpack 中一切皆模块,经 loader 处理后的 CSS 可以在 js 中被直接引用。

1. css-loader 与 style-loader

// 引入 css-loader
npm install --save-dev css-loader

// webpack.config.js 配置loader
{
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['css-loader']
            }
        ];
    }
}

// 代码中引入 css 模块
import css from './css/index.css';
console.log(css);

// 或者不用webpack配置文件,直接在引入语句中使用loader
import css from 'css-loader!./css/index.css';
console.log(css);

经 css-loader 处理后,css 模块被打包转换成 js 对象,可直接用 js 语法使用。

style-loader 的作用是将 css-loader 打包好的 css 代码以<style>标签的形式插入到 html 文件中,一般情况下,在一个项目中 style-loader 与 css-loader 是成对出现的,并且 style-loader 要在 css-loader 之后。

npm install --save-dev style-loader

2. mini-css-extract-plugin

CSS 作为<style>标签放到 HTML 内还是不够的,我们还需要将 CSS 以 <link> 的方式通过 URL 的方式引入进来,这时候就需要使用 mini-css-extract-plugin

npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    plugins: [
        // 添加 plugin
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[id].css'
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                // 添加 loader
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ]
    }
};

3. CSS Modules

CSS Modules 指的是所有的 CSS 类名及其动画名都只是局部作用域的 CSS 文件。CSS Modules 主要解决的问题有:

  • 解决 CSS 类都是全局的,容易造成全局污染(样式冲突);
  • JS 和 CSS 共享类名;
  • 可以方便的编写出更加健壮和扩展方便的 CSS。

CSS Module 的表现形式:

/* app.css */
.element {
    background-color: blue;
    color: white;
    font-size: 24px;
}
/* app.js */
// 引入 app.css
import styles from './app.css';
// js 直接使用 CSS 的类名作为对象值
let element = `
  <div class="${styles.element}">
    <p>CSS Modules</p>
  </div>
`;
document.write(element);

CSS Modules 在 webpack.config.js 中的配置:

// CSS Modules 相关的配置还有很多,可以查看css-loader对应的文档。
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true
                        }
                    }
                ]
            }
        ]
    }
};

4. CSS 预处理器

常见的 CSS 预处理器有 Less、Sass 及其语法变种 Scss和Stylus。预处理器为 CSS 提供了变量、函数、运算、作用域、继承、嵌套等语法,使其功能更加强大。

以 less-loader 为例,less-loader 将 Less 语法编译成 CSS,后续还需要使用 css-loader 和 style-loader 处理才可以,所以一般来说需要配合使用:

module.exports = {
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true
                        }
                    },
                    'less-loader' // 将 Less 编译为 CSS
                ]
            }
        ]
    }
};

一些预处理语言需要安装对应的解析器,例如 sass-loader,需要同时安装 node-sass。

npm install sass-loader node-sass --save-dev

5. PostCSS:CSS 后处理器

表现形式:

/*没有前缀的写法*/
.flex {
    display: flex;
}

/*经过 postcss autoprefixer 处理后*/
.flex {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}

PostCss 能实现的功能很多,除了添加前缀,还可以最新语法转义、压缩等,甚至可以扩展 CSS 的语言特性。配置了 postcss-loader 之后,WebPack 就可以使用 PostCSS 来处理 CSS了,但是 PostCSS 本身只不过是将 CSS 解析成 AST ,真正起作用的还需要依赖其强大的插件系统。

PostCSS 配置其实主要是配置其使用哪些插件,PostCSS 的配置写法有以下三种方式:

  • 通过配置文件postcss.config.js,一般放置在项目的根目录下;
  • 通过 loader 的配置项 options;
  • 直接在 package.json 中添加个 postcss 属性。
/* postcss.config.js */
// 引入postcss 插件
const autoprefixer = require('autoprefixer');
module.exports = {
    plugins: [autoprefixer(['IE 10'])]
};
/* webpack.config.js */
// 引入postcss 插件
const autoprefixer = require('autoprefixer');
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            // 通过 plugins 选项
                            plugins: [autoprefixer(['IE 10'])]
                        }
                    }
                ]
            }
        ]
    }
};
/* package.json:受限于 json 的语法,可扩展性较弱,一般不推荐 */
{
    "postcss": {
        "plugins": {
            "autoprefixer": "IE 10"
        }
    }
}

6. 常见的 PostCss 插件

以下罗列几种常用的 PostCSS 插件,详细用法参见各自的官网或相关文档。

Autoprefixer

给 css 补齐各种浏览器私有的前缀,例如 -webkit、-moz、-ms 等,当然还会处理各种兼容性问题,比如 flex 语法,不能简单添加 -webkit 就解决,还需要处理成 -webkit-box 这类老版本的标准。

Autoprefixer 的主要参数就是 browserslist。

postcss-preset-env

postcss-preset-env 是跟 babel 的 preset-env 类似的功能,不用一一引入要用的插件,在打包构建的时候,会根据不同的配置输出对应支持的 CSS 文件。

PreCSS

可以写类似 Sass 和 cssnext 语法的 CSS。

cssnano

CSS 压缩优化。

四 代码规范工具配置

1. ESLint

npm install eslint --save-dev

安装之后,可以通过使用 npx eslint 直接运行,在运行之前,我们需要创建个 ESLint 的配置文件,使用 eslint --init 命令创建 .eslintrc.json 文件。

.eslintrc.json 内容示例,具体可查看 eslint 官网文档:

{
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": "airbnb-base",
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "rules": {
        // 禁止 console,要用写 eslint disbale
        'no-console': 2,
        // 禁止 debugger,防止上线
        'no-debugger': 2,
        // 禁止 alert,要用写 eslint disable
        'no-alert': 2,
        // 不用的 var,要删除,手动 tree shaking,要洁癖
        'no-unused-vars': 2,
        // 没定义就用的就别用,全局的要用 写 eslint global
        'no-undef': 2
    }
}

ESLint 的报错类型包括三种:off、warn和error,分别对应着:0、1、2,所以上面的配置的 rule 实际为error级别的规则,检测到了则直接报错误(Error)。

webpack 中的配置:

npm install  eslint-loader --save-dev
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'eslint-loader',
                enforce: 'pre', // 调整了 loader 加载顺序,保证先检测代码风格,之后再做 Babel 转换等工作
                include: [path.resolve(__dirname, 'src')], // 指定检查的目录
                options: { // 这里的配置项参数将会被传递到 eslint 的 CLIEngine
                    formatter: require('eslint-friendly-formatter') // 指定错误报告的格式规范
                }
            }
        ]
    }
};

2. StyleLint

StyleLint 的配置文件是.stylelintrc.json,其中的写法跟 ESLint 的配置类似

npm install -D stylelint
/* .stylelintrc.json */
{
    "extends": ["stylelint-config-standard", "stylelint-config-recess-order"],
    "rules": {
        "at-rule-no-unknown": [true, {"ignoreAtRules": ["mixin", "extend", "content"]}]
    }
}

webpack 中配置:

const StyleLintPlugin = require('stylelint-webpack-plugin');

module.exports = {
    // ...
    plugins: [new StyleLintPlugin(options)]
    // ...
};

默认 StyleLint-webpack-plugin 会查找项目中的 StyleLint 配置文件,根据配置文件的配置来检测 CSS 代码。

五 静态资源管理

前端的静态资源一般指图片、字体文件、富媒体等。

1. 图片引入方式

图片引入的一般方式有,Html 引入,Css 引入和 Javascript 的引入。在 webpack 中可以使用 loader 来处理加载图片。

file-loader 与 url-loader

这是最常见的处理图片的两种 loader。它俩区别在于:

  • file-loader: 能够根据配置项复制使用到的资源和构建之后的文件夹,并且能够更改对应的链接
  • url-loader: 包含 file-loader 的全部功能,并且能欧根据配置将文件转换成 Base64 的形式,以此减少 http 请求,实现一定程度上的优化。如果图片较大,Base64 会很长,会导致打包后的 js/css 文件很大。

webpack 的配置如下,使用 publicPath 可做静态资源的 CDN 处理,可以使用 resolve.alias 做别名处理:

// webpack.config.js
const HTMLPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    // output: {
    //     publicPath: 'http://bd.bxstatic.com/img/'
    // }
    devtool: false,
    module: {
        // resolve: {
        //     alias: {
        //         '@assets': path.resolve(__dirname, './src/assets')
        //     }
        // },
        rules: [
            {
                test: /\.html$/,
                use: ['html-loader']
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // url-loader 默认会做 Base64 处理,这里限制不超过 3k 大小的图片才做 Base64 处理
                        limit: 3*1024
                    }
                }
            }
        ]
    },
    plugins: [
        new HTMLPlugin({
            template: './src/index.html'
        })
    ]
};

在 Html 和 Css 中用别名的方式引入图片时,要在别名前加~符号:

<img src="~@assets/img/large.png" alt="背景图" />
.bg-img {
    background: url(~@assets/img/small.png) no-repeat;
}

svg-url-loader

svg-url-loader 的工作原理类似于 url-loader,主要用来处理 SVG 文件。 svg-url-loader 拥有改善 IE 浏览器支持的选项,但是在其他浏览器中很糟糕。如果你需要兼容 IE 浏览器,设置 iesafe: true选项。

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/,
                loader: 'svg-url-loader',
                options: {
                    // 小于 10kB(10240字节)的内联文件
                    limit: 10 * 1024,
                    // 移除 url 中的引号
                    // (在大多数情况下它们都不是必要的)
                    noquotes: true
                }
            }
        ]
    }
};

img-webpack-loader

img-webpack-loader 用来对图片进行压缩优化,支持 JPG、PNG、GIF 和 SVG 格式的图片,不过必须和 url-loader 以及 svg-url-loader 一起使用。

module.exports = {
    module: {
        rules: [
            {
                test: /\.(jpe?g|png|gif|svg)$/,
                loader: 'image-webpack-loader',
                //提高优先级,保证在url-loader和svg-url-loader之前完成图片优化
                enforce: 'pre'
            }
        ]
    }
};

postcss-sprites

将小图标合成雪碧图。需要结合 postcss-loader 使用。

// postcss.config.js
const postcssSprites = require('postcss-sprites');

module.exports = {
    plugins: [
        postcssSprites({
            // 在这里制定了从哪里加载的图片被主动使用css sprite
            // 可以约定好一个目录名称规范,防止全部图片都被处理
            spritePath: './src/assets/img/'
        })
    ]
};
//webpack.config.js
module.exports = {
    module: {
        rules: [
            {
            test: /\.css$/,
            use: [
                MiniCssExtractPlugin.loader,
                'css-loader',
                {
                    loader: 'postcss-loader'
                }
            ]
        }
        ]
    }
};

2. 字体、富媒体处理

可以直接使用 url-loader 或者 file-loader 进行配置即可,不需要额外的操作。如果不需要 Base64,那么可以直接使用 file-loader,需要的话就是用 url-loader。

{
    // 文件解析
    test: /\.(eot|woff|ttf|woff2|appcache|mp4|pdf)(\?|$)/,
    loader: 'file-loader',
    query: {
        // 这么多文件,ext不同,所以需要使用[ext]
        name: 'assets/[name].[hash:7].[ext]'
    }
}

3. 数据

类似 CSV、TSV 和 XML 等数据,那么我们需要单独给它们配置相应的 loader。内置支持 JSON 数据。

 {
    test: /\.(csv|tsv)$/,
    use: [
    'csv-loader'
    ]
},
{
    test: /\.xml$/,
    use: [
    'xml-loader'
    ]
}

六 HTML 打包配置

webpack 处理 html 页面,一般是使用 html-webpack-plugin 插件。

// 安装
npm install html-webpack-plugin --save-dev

简单配置示例

const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    plugins: [new HtmlWebPackPlugin()]
    // 指定打包后页面的名称(默认index.html)和 title
    // plugins: [new HtmlWebPackPlugin({title: 'hello', filename: 'foo.html'})]
};

打包后会生成 index.html,内容如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Webpack App</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
    </head>

    <body>
        <script src="main.js"></script>
    </body>
</html>

指定Template打包

指定一个 template,使打包后的 html 页面是依据这个 template 来打包的,这样 html 的内容我们可以自定义,比如我们需要打包后的文件有<div id="app"></div>节点。

指定 template:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Webpack</title>
    </head>
    <body>
        <h1>hello world</h1>
        <div id="app"></div>
    </body>
</html>

webpack.config.js 配置:

const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: './src/index.html' // 指定模板
        })
    ]
};

打包后的 html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Webpack</title>
</head>
<body>
    <h1>hello world</h1>
    <div id="app"></div>
<script src="main.js"></script></body>
</html>

多页配置

单一入口:

const HtmlWebPackPlugin = require('html-webpack-plugin');

const indexPage = new HtmlWebPackPlugin({
    template: './src/index.html',
    filename: 'index.html'
});
const listPage = new HtmlWebPackPlugin({
    template: './src/list.html',
    filename: 'list.html'
});

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    plugins: [indexPage, listPage]
};

多入口:

const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.js',
        list: './src/list.js'
    },
    plugins: [
        new HtmlWebPackPlugin({template: './src/index.html', filename: 'index.html', chunks: ['index']}),
        new HtmlWebPackPlugin({template: './src/list.html', filename: 'list.html', chunks: ['list']})
    ]
};

最佳实践:

/**
 * 1. /scripts/utils/index.js 
 * 使用 npm 库 globby 读取src/pages/*.js, 生成入口 entry
 */
const path = require('path');
const globby = require('globby');

const getEntry = (exports.getEntry = () => {
    // 异步方式获取所有的路径
    const paths = globby.sync('./pages/*.js', {
        cwd: path.join(__dirname, './src')
    });
    const rs = {};
    paths.forEach(v => {
        // 计算 filename
        const name = path.basename(v, '.js');
        let p = path.join('./src', v);
        if (!p.startsWith('.')) {
            // 转成相对地址
            p = './' + p;
        }

        rs[name] = p;
    });
    return rs;
});

console.log(getEntry());

/**
 * 2. /scripts/utils/index.js
 * 遍历 entry,生成 html-webpack-plugins 数组
 */
const HtmlWebPackPlugin = require('html-webpack-plugin');

exports.getHtmlWebpackPlugins = () => {
    const entries = getEntry();
    return Object.keys(entries).reduce((plugins, filename) => {
        plugins.push(
            new HtmlWebPackPlugin({
                template: entries[filename],
                filename: `${filename}.html`,
                chunks: [filename]
            })
        );
        return plugins;
    }, []);
};

/**
 * 3. webpack.config.js
 * require 封装好的html-webpack-plugins 数组
 */

const {getEntry, getHtmlWebpackPlugins} = require('./scripts/utils');

module.exports = {
    mode: 'development',
    getEntry(),
    plugins: [
        ...getHtmlWebpackPlugins()
    ]
};

七 Webpack Dev Server 本地开发服务

webpack-dev-server 是一个可以用来启动本地服务的插件。通过命令启动对应的服务。

// 安装
npm install webpack-dev-server
// 启动命令
npx webpack-dev-server

/* 命令常用参数 */
// 修改端口号和 host
webpack-dev-server --port 3000 --host 127.0.0.1
// 启动inline 模式的自动刷新
webpack-dev-server --hot --inline
// 手动指定 webpack config 文件
webpack-dev-server --config webpack.xxx.js
// 指定 webpack 的 mode
webpack-dev-server --mode development
// watch 功能,文件发生变化则触发重新编译
webpack-dev-server --watch
// dev-server默认会将工作目录(当前目录)作为基本目录,可以手动修改它
webpack-dev-server --content-base ./build

执行 webpack-dev-server 命令之后,它会读取 Webpack 的配置文件(默认是 webpack.config.js),然后将文件打包到内存中(所以看不到dist文件夹的生产,Webpack 会打包到硬盘上),这时候打开 server 的默认地址:localhost:8080 就可以看到文件目录或者页面(默认是显示 index.html,没有则显示目录)。

一般在项目开发中,启动本地服务的命令会配置在 package.json 的 scripts 属性中:

{
    "scripts": {
        "dev": "webpack-dev-server --mode development --config webpack.config.dev.js --hot --inline --port 3000"
    }
}

更多的时候,是通过 devServer 属性直接配置在 webpack.config.js 中:

const path = require('path');
module.exports = {
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        port: 9000, // 端口
        hot: true, // 热部署,开启热部署需要引入 HotModuleReplacementPlugin 插件
        inline: true,
        // proxy解决本地开发跨域的问题,/api所有请求都转发到了baidu.com
        proxy: {
            '/api': 'http://baidu.com'
            // secure: false // 支持 https
        }
        // 加载所有内部中间件之前和之后可以插入自定义的中间件
        before(app, server) {
            app.get('/api/mock.json', (req, res) => {
                res.json({hello: 'world'});
            });
        },
        after(){}

    },
    plugins: [
        // 添加 hmr plugin
        new webpack.HotModuleReplacementPlugin()
    ]
};

HMR 即模块热替换(Hot Module Replacement)的简称,它可以在应用运行的时候,不需要刷新页面,就可以直接替换、增删模块。

八 注意事项

开发环境和生产环境的配置可能不同,需要分开、判断处理。

开发 vue 一般通过集成了 webpack 的 vue-cli 创建项目,但如果直接使用 webpack 需要引入 vue-loader


记忆思考:

1.什么是 JavaScript 的模块化开发?有哪些可以遵循的规范?

2.在 js 文件中怎么调用 loader 来处理一个模块?

4.怎么实现 Webpack 的按需加载?什么是神奇注释?

5.Babel 的 preset-env 是什么?

6.Babel 怎么做 Polyfill,Polyfill 的最佳实践是什么?

7.Babel 怎么针对不同的浏览器打包不同的适配代码

8.Webpack 的 style-loader 和 css-loader 有什么区别

9.怎么使用 PostCSS 来处理 CSS

10.Webpack 中怎么给静态资源添加上 CDN 域名?

11.url-loader 和 file-loader 有什么区别?

12.怎么配置 Webpack 的多页面开发?