webpack 插件拾趣 (1) —— webpack-dev-server

结束了一季的忙碌,我这封笔已久的博客也终究该从春困的咒印中复苏,想来写些实用易读的作为开篇,自然是最好不过。

新开个 webpack 插件/工具介绍的文章系列,约莫每周更新一篇篇幅适中的文章聊以共勉,兴许合适。

原本期望每篇文章里可以介绍若干个插件,但鉴于部分插件略为复杂,且单篇内容不想写的唇焦舌敝惹人倦烦,所以像本文要介绍的 webpack-dev-server 就独立一文了。

回归主题,今天你或许会花上30分钟的时间读完本章,并掌握 webpack-dev-server 的使用方法、理清一些容易困惑的配置(诸如 publicPath)或概念(如HMR)

另外,本章涉及的相关用例,可以在我的github(https://github.com/VaJoy/webpack-plugins/tree/master/char1)上下载到。

一. webpack-dev-server 他爹和他爹的朋友

我们并不急着把 webpack-dev-server 直接拉出来介绍一通,我们先了解下他的两位长辈 —— 他爹 webpack-dev-middleware,以及他爹的朋友 webpack-hot-middleware

他们三人有着某些亲密的联系,不少读者可能会对其身份存在认知混乱,所以很有必要按辈分次序来分别介绍。

1.1 webpack-dev-middleware

假设我们在服务端使用 express 开发一个站点,同时也想利用 webpack 对静态资源进行打包编译,那么在开发环节,每次修改完文件后,都得先执行一遍 webpack 的编译命令,等待新的文件打包到本地,再做进一步调试。虽然咱们可以利用 webpack 的 watch mode 来监听变更、自动打包,但等待 webpack 重新执行的过程往往很耗时。

而 webpack-dev-middleware 的出现很好地解决了上述问题 —— 作为一个 webpack 中间件,它会开启 watch mode 监听文件变更,并自动地在内存中快速地重新打包、提供新的 bundle。

说白了就是 —— 自动编译(watch mode)+速度快(全部走内存)

webpack-dev-middleware 的配置与使用其实很轻松,我们通过一个非常简单的项目来示例(可以点这里获取):

PROJECT
│  app.js  //应用入口文件
│  express.config.js   // express 服务启动配置
│  package.json
│  webpack.config.js  // webpack 配置
│
└─src
    ├─html
    │      index.html  //首页
    │
    └─js
        └─page
                index.js  //首页脚本模块

它的 webpack.config.js 配置文件如下:

module.exports = {
    entry: ‘./app.js‘,
    output: {
        publicPath: "/assets/",
        filename: ‘bundle.js‘,
        //path: ‘/‘   //只使用 dev-middleware 的话可以忽略本属性
    }
};

这里有一个非常关键的配置 —— publicPath,熟悉 webpack 的同学都知道,它是生成的新文件所指向的路径,可以用于模拟 CDN 资源引用。

打个比方,当我们使用 url-loader 来处理图片时,把 publickPath 设为“http://abcd/assets/”,则最终打包后,样式文件里所引用的图片地址会加上这个前缀:

/**-------------webpack配置项--------------**/
    output: {
        publicPath: "http://abcd/assets/",   //模拟CDN地址
        filename: ‘bundle.js‘,
        path: path.join(__dirname, ‘dist/‘)
    },
    module: {
        rules: [{
                test: /\.css$/,
                loader: [‘style-loader‘, ‘css-loader‘]
            },
            {
                test: /\.(png|jpg|gif)$/,
                loader: ‘url-loader‘
            }]
    }

/**-------------页面引入的样式模块 index.css--------------**/
section{
    width:300px;
    height: 300px;
    background-image: url(a.jpg);
}

打包后(dist/bundle.js 里的样式执行效果)

当然如果你没把资源(比如这张md5化后的图片)托管到CDN上,是请求不到的,不过通过Fiddler配置代理映射,可以解决这个问题。

然而,在使用 webpack-dev-middleware (或其它走内存的工具)的情况下,publicPath 只建议配置相对路径 —— 因为 webpack-dev-middleware 在使用的时候,也需要再配置一个 publicPath(见下文 express.config.js 的配置),用于标记从内存的哪个路径去存放和查找资源,这意味着 webpack-dev-middleware 的 publicPath 必须是相对路径。

而如果 webpack.config.js 里的 publicPath 跟 webpack-dev-middleware 的 publicPath 不一致的话(比如前者配置了 http 的路径),会导致资源请求到了内存外的地方去了(本地也没这个文件,也没法走 Fiddler 代理来解决),从而返回404~

如果上面这段话瞧着糊涂,建议暂时搁置它,后续回过头再来咀嚼,我们先了解下所谓的“webpack-dev-middleware 的 publicPath”是什么。

如下是 express.config.js 文件:

const path = require(‘path‘);
const express = require("express");
var ejs = require(‘ejs‘);
const app = express();
const webpack = require(‘webpack‘);
const webpackMiddleware = require("webpack-dev-middleware");
let webpackConf = require(‘./webpack.config.js‘);

app.engine(‘html‘, ejs.renderFile);
app.set(‘views‘, path.join(__dirname, ‘src/html‘));
app.set("view engine", "html");

var compiler = webpack(webpackConf);

app.use(webpackMiddleware(compiler, {  //使用 webpack-dev-middleware
    publicPath: webpackConf.output.publicPath  //保持和 webpack.config.js 里的 publicPath 一致
}));

app.get("/", function(req, res) {
    res.render("index");
});

app.listen(3333);

可见 webpack-dev-middleware 的使用语法其实就这么简练,不外乎是:

var webpackMiddleware = require("webpack-dev-middleware");

app.use(webpackMiddleware(webpack(webpackConfig), options));

其中 options 是 webpack-dev-middleware 的配置对象,详尽的可选项可参考官方文档,限于篇幅,此处只介绍 publicPath —— 它用于决定 webpack 打包编译后的文件,要存放在内存中的哪一个虚拟路径,并提供一个 SERVER,将路径和文件映射起来(即使它们都是虚拟的,但依旧可请求的到)

当前的例子,是将内存路径配置为 /assets/,这意味着打包后的 bundle.js 会存放在虚拟内存路径 SERVERROOT/assets/ 下(这里的“SERVERROOT”实际上即 html 文件的访问路径),也意味着我们可以直接在 src/html/index.html 中通过 src=‘assets/bundle.js‘ 的形式引用和访问内存中的 bundle 文件:

<body>
    <div></div>
    <script src="assets/bundle.js"></script>
</body>

我们执行一遍 node express.config,然后访问 http://localhost:3333,便能正常访问页面、请求和执行 bundle.js:

同时,只要我们修改了页面的脚本模块(比如 src/js/index.js),webpack-dev-middleware 便会自行重新打包到内存,替换掉旧的 bundle,我们只需要刷新页面即可看到刚才的变更。

这里写个关于 webpack-dev-middleware 的小 tips:

1. webpack-dev-middleware 配置项里的 publicPath 要与 webpack.config 里的 output.publicPath 保持一致(并且只能是相对路径),不然会出现问题;
2. 使用 webpack-dev-middleware 的时候,其实可以完全无视 webpack.config 里的 output.path,甚至不写也可以,因为走的纯内存,output.publicPath 才是实际的 controller;
3. publicPath 配置的相对路径,实际是相对于 html 文件的访问路径。

1.2 HMR

机智的小伙伴们在读完 webpack-dev-middleware 的介绍后,会洞悉出它的一处弱点 —— 虽然 webpack-dev-middleware 会在文件变更后快速地重新打包,但是每次都得手动刷新客户端页面来访问新的内容,还是略为麻烦。这是因为 webpack-dev-middleware 在应用执行的时候,没办法感知到模块的变化。

那么是否有办法可以让页面也能自动更新呢?webpack-hot-middleware 便是帮忙填这个坑的人,所以我在前文称之为 —— webpack-dev-middleware 的好朋友。

webpack-hot-middleware 提供的这种能力称为 HMR,所以在介绍 webpack-hot-middleware 之前,我们先来科普一下 HMR。

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

webpack 可以通过配置 webpack.HotModuleReplacementPlugin 插件来开启全局的 HMR 能力,开启后 bundle 文件会变大一些,因为它加入了一个小型的 HMR 运行时(runtime),当你的应用在运行的时候,webpack 监听到文件变更并重新打包模块时,HMR 会判断这些模块是否接受 update,若允许,则发信号通知应用进行热替换。

这里提及的“判断模块是否接受 update”是指判断模块里是否执行了 module.hot.accept(), 这里举个小例子:

如图,白色的部分是编译后的模块依赖树,这时候我们修改了 B 模块,导致 B 模块以及依赖它的 A 模块都出现了变化(绿色部分)

模块变更的时候,webpack 会顺着依赖树一层一层往上冒泡,查询哪个模块是接受 update 的,查询到了则终止冒泡,并通知 SERVER 更新其爬过的模块。

假设我们把 module.hot.accept() 放在 B 模块执行,则 webpack 会查找到 B` 模块的变更就停止继续往上冒泡查找了(A`是不允许变更的模块)—— 如果 B 的内容变更,是直接在 B 模块调用的,那页面就能直接展示出新的内容出来,这样效率也高(绕过了A模块);但如果 B 的内容,实际上是要经过 A 来调用,才能在页面上展示出来,那此时页面就不会刷新(即使 B 的内容变了)

说白了就是 module.hot.accept() 放的好,就可以绕过一些不必要的模块变更检查来提升效率,不过对于懒人来说,直接置于最顶层的模块(比如入口模块)最为省心。

关于更多的 HMR 的知识点,可以参考官方文档

1.3 webpack-hot-middleware

聊完了 HMR,我们回头了解下 webpack-hot-middleware 的使用。

我们试着对前文使用的项目来做一番改造 —— 引入 webpack-hot-middleware 来提升开发体验。

首先往 express.config.js 加上一小段代码:

app.engine(‘html‘, ejs.renderFile);
app.set(‘views‘, path.join(__dirname, ‘src/html‘));
app.set("view engine", "html");

var compiler = webpack(webpackConf);

app.use(webpackMiddleware(compiler, {
    publicPath: webpackConf.output.publicPath
}));

//添加的代码段,引入和使用 webpack-hot-middleware
app.use(require("webpack-hot-middleware")(compiler, {
    path: ‘/__webpack_hmr‘
}));

app.get("/", function(req, res) {
    res.render("index");
});

app.listen(3333);

即在原先的基础上引入了 webpack-hot-middleware:

app.use(require("webpack-hot-middleware")(webpackCompiler, options));

这里的 options 是 webpack-hot-middleware 的配置项,详细见官方文档,这里咱们只填一个必要的 path —— 它表示 webpack-hot-middleware 会在哪个路径生成热更新的事件流服务,且访问的页面会自动与这个路径通过 EventSource 进行通讯,来拉取更新的数据重新粉饰自己。

这里要了解下,实际上 webpack-hot-middleware 最大的能力,是让 SERVER 能够和 HMR 运行时进行通讯,从而对模块进行热更新。

然后是 webpack.config.js 文件:

const path = require(‘path‘);
const webpack = require(‘webpack‘);
module.exports = {
    entry: [‘webpack-hot-middleware/client‘, ‘./app.js‘],  //修改点1
    output: {
        publicPath: "/assets/",
        filename: ‘bundle.js‘
    },
    plugins: [  //修改点2
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()   //出错时只打印错误,但不重新加载页面
    ]
};

首先是 entry 里要多加上 ‘webpack-hot-middleware/client‘,此举是与 server 创建连接。

接着加上两个相关的插件来打通 webpack HMR 的任督二脉,其中的 webpack.HotModuleReplacementPlugin 我们在上一节提及过,它是 HMR 的功能提供者。

最后一步很重要,很多新手容易漏掉。我们需要在入口文件 app.js 里加上一小段代码:

import {init} from ‘./src/js/page/index‘;

//灰常重要,知会 webpack 允许此模块的热更新
if (module.hot) {
    module.hot.accept();
}

init();

此处的 module.hot.accept() 是知会 webpack 接受此模块的 HMR update,在上一节已经提及多次。

补充好上述的代码,执行 node express.config 并访问 http://localhost:3333,之后的模块修改,都会自动打包并更新客户端页面模块:

1.4 webpack-dev-server

虽然 webpack-dev-middleware + webpack-hot-middleware 的组合为开发过程提供了便利,但它们仅适用于服务侧开发的场景。

很多时候我们仅仅对客户端页面做开发,没有直接的 server 来提供支持,这时候就需要 webpack-dev-server 来解囊相助了。

顾名思义,webpack-dev-server 相对前两个工具多了个“server”,实际上它的确也是在 webpack-dev-middleware 的基础上多套了一层壳来提供 CLI 及 server 能力(这也是为何我称 webpack-dev-middleware 是 webpack-dev-server 他爹)

此处依旧以一个简单的项目来展示如何配置、使用 webpack-dev-server,你可以点这里获取相关代码。

脱离了 express,我们不再需求配置后端脚本,不过对于 webpack.config.js,需要多加一个名为“devServer”的  webpack-dev-server 配置项:

const path = require(‘path‘);
module.exports = {
    entry: ‘./app.js‘,
    output: {
        publicPath: "/assets/",
        filename: ‘bundle.js‘
    },
    devServer: { //新增配置项
        contentBase: path.join(__dirname, "src/html"),
        port: 3333
    }
};

其中 devServer.port 表示 SERVER 的监听端口,即运行后我们可以通过 http://localhost:3333 来访问应用;

而 devServer.contentBase 表示 SERVER 将从哪个目录去查找内容文件(即页面文件,比如 HTML)

确保安装好 webpack-dev-server 后执行其 CLI 命令来召唤支持热更新的 SERVER:

webpack-dev-server

接着访问 http://localhost:3333,似乎便能获得前文 webpack-dev-middleware + webpack-hot-middleware 的热更新能力~

不过事实并非如此,虽然在我们修改模块后,页面的确自动刷新了。但截止此处,webpack-dev-server 跑起来其实只相当于捎上了 SERVER 的 webpack-dev-middleware,而没有 HMR —— 在我们修改应用模块后,页面是整个刷新了一遍,而并非热更新。

希望读者们可以记住,HMR 提供了局部更新应用模块的能力,而不需要刷新整个应用页面

这块的验证也很简单,直接在 index.html 里加个 script 打印 Date.now() 即可,若刷新页面,打印的值直接会变。

要让 webpack-dev-server 加上 HMR 的翅膀,其实就得像前面 webpack-hot-middleware 的配置那样,把 HMR 相关的东西通通加上,同时将 devServer.hot 设为 true:

// webpack.config.js
const path = require(‘path‘);
const webpack = require(‘webpack‘);
module.exports = {
    entry: ‘./app.js‘,
    output: {
        publicPath: "/assets/",
        filename: ‘bundle.js‘
    },
    devServer: {
        contentBase: path.join(__dirname, "src/html"),
        port: 3333,
        hot: true  // 让 dev-server 开启 HMR
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()  //让 webpack 启动全局 HMR
    ]
};
// 入口文件 app.js

import {init} from ‘./src/js/page/index‘;
if (module.hot) {
    // 知会 webpack 该模块接受 HMR update
    module.hot.accept();
}
init();

这时候,再执行 webpack-dev-server,才是正宗的有 HMR 加持的 SERVER。

关于完整的 devServer 配置项可参考官方文档,在文章的最后,我们罗列几个常用项做简单介绍。

1. contentBase

即 SERVERROOT,如上方示例配置为 “path.join(__dirname, "src/html")”,后续访问 http://localhost:3333/index.html 时,SERVER 会从 src/html 下去查找 index.html 文件。

它可以是单个或多个地址的形式:

contentBase: path.join(__dirname, "public")
//多个:
contentBase: [path.join(__dirname, "public"), path.join(__dirname, "assets")]

若不填写该项,默认为项目根目录。

2. port

即监听端口,默认为8080。

3. compress

传入一个 boolean 值,通知 SERVER 是否启用 gzip。

4. hot

传入一个 boolean 值,通知 SERVER 是否启用 HMR。

5. https

可以传入 true 来支持 https 访问,也支持传入自定义的证书:

https: true
//也可以传入一个对象,来支持自定义证书
https: {
  key: fs.readFileSync("/path/to/server.key"),
  cert: fs.readFileSync("/path/to/server.crt"),
  ca: fs.readFileSync("/path/to/ca.pem"),
}

6. proxy

代理配置,适用场景是,除了 webpack-dev-server 的 SERVER(SERVER A) 之外,还有另一个在运行的 SERVER(SERVER B),而我们希望能通过 SERVER A 的相对路径来访问到 SERVER B 上的东西。

举个例子:

    devServer: {
        contentBase: path.join(__dirname, "src/html"),
        port: 3333,
        hot: true,
        proxy: {
            "/api": "http://localhost:5050"
        }
    }

运行 webpack-dev-server 后,你若访问 http://localhost:3333/api/user,则相当于访问 http://localhost:5050/api/user。

更多可行的 proxy 配置见 https://webpack.js.org/configuration/dev-server/#devserver-proxy,这里不赘述。

7. publicPath

如同 webpack-dev-middleware 的 publicPath 一样,表示从内存中的哪个路径去存放和检索静态文件。

不过官方文档有一处错误需要堪正 —— 当没有配置 devServer.publicPath 时,默认的 devServer.publicPath 并非根目录,而是 output.publicPath:

这也是为何咱们的例子里压根没写 devServer.publicPath,但还能正常请求到 https://localhost:3333/assets/bundle.js。

8. setup

webpack-dev-server 的服务应用层使用了 express,故可以通过 express app 的能力来模拟数据回包,devServer.setup 方法就是干这事的:

    devServer: {
        contentBase: path.join(__dirname, "src/html"),
        port: 3333,
        hot: true,
        setup(app){  //模拟数据
            app.get(‘/getJSON‘, function(req, res) {
                res.json({ name: ‘vajoy‘ });
            });
        }
    }

然后我们可以通过请求 http://localhost:3333/getJSON 来取得模拟的数据:

时间: 2024-11-05 19:35:18

webpack 插件拾趣 (1) —— webpack-dev-server的相关文章

webpack插件机制

webpack插件机制是整个webpack工具的核心,那么webpack插件有什么特点呢? 1.独立的JS模块,暴露相应的函数 2.函数原型上的apply方法会注入compiler对象(之所以要定义apply方法,是因为源码中是通过plugin.apply()调用插件的) 3.compiler对象上挂载了相应的webpack事件钩子 4.事件钩子的回调函数里能拿到编译后的compilation对象,如果是异步钩子还能拿到相应的callback 下面看个例子: function MyPlugin(

webpack插件之htmlWebpackPlugin

webpack插件之htmlWebpackPlugin webpack插件 自动化 htmlWebpackPlugin 由于webpack已经帮我们处理好js之间的依赖关系,现在我们可以忽略js的加载顺序,而只要在index.html内使用<script>标签引入bundle.js即可. 在index.html内使用引入bundle.js 开发阶段,index.html在根目录,script引入好像也没什么问题. index.html在根目录 但在真实发布项目时,发布的内容js文件都在dist

[Webpack] Access Webpack Dev Server from Mobile Safari on an iPhone

Testing your sites on mobile devices is a critical part of the development process. Webpack dev server enables you to visit the server from any device using the host option. This lesson walks you through accessing webpack dev server using an iPhon Ch

解决新版本webpack vue-cli生成文件没有dev.server.js问题

新版本webpack生成的dev.server.js 在webpack.dev.conf.js中 webpack.dev.conf.js const axios = require('axios') const express = require('express') const app = express() const apiRoutes = express.Router() app.use('/api', apiRoutes) 然后找到devserver 这里可以配置路由 devServe

笔记:配置 webpack dev server

笔记:配置 webpack dev server 安装 webpack-dev-server 组件 配置 webpack.config.js 配置 增加 html-webpack-plugin 组件 启动 webpack-dev-server 原文地址:https://www.cnblogs.com/F4NNIU/p/webpack-dev-server.html

webpack插件之webpack-dev-server

webpack插件之webpack-dev-server webpack插件 自动化 webpack-dev-server 现在只需要使用 npm run build指令就可以自动打包,并自动处理好各种依赖关系,但还是存在一个问题, 如果对js文件的代码进行了修改,又要重新打包才能测试,很明显又要手动操作 在开发测试过程中,我们会经常修改代码后,然后频繁刷新页面查看效果.对于我们前端仔来说,每次修改代码后都需要重新编译才能测试.这个过程非常繁琐,这简直是个深坑,不能忍啊.使用webpack-de

webpack 插件: html-webpack-plugin

插件地址:https://www.npmjs.com/package/html-webpack-plugin 这个插件用来简化创建服务于 webpack bundle 的 HTML 文件,尤其是对于在文件名中包含了 hash 值,而这个值在每次编译的时候都发生变化的情况.你既可以让这个插件来帮助你自动生成 HTML 文件,也可以使用 lodash 模板加载生成的 bundles,或者自己加载这些 bundles. Installation 使用 npm 安装这个插件 $ npm install

webpack 插件: html-webpack-plugin的使用

插件地址:https://www.npmjs.com/package/html-webpack-plugin 这个插件用来简化创建服务于 webpack bundle 的 HTML 文件,尤其是对于在文件名中包含了 hash 值,而这个值在每次编译的时候都发生变化的情况.你既可以让这个插件来帮助你自动生成 HTML 文件,也可以使用 lodash 模板加载生成的 bundles,或者自己加载这些 bundles. Installation 使用 npm 安装这个插件 $ npm install

编写webpack 插件

Webpack插件为第三方开发者释放了Webpack的最大可能性.利用多级回调开发者可以把他们自己的需要的功能引入到Webpack里面来.Build插件比Build loader 更进一步.因为你需要理解Webpack底层的东西.要有月底源代码的准备. Compiler 和 Compilation 开发插件最重要的两个资源就是 compiler 和 compilation 对象,理解他们的是扩展Webpack重要的一步 compiler对象包涵了Webpack环境所有的的配置信息,这个对象在We