webpack中构建本地服务,最重要的一个插件webpack-dev-server,我们俗称WDS,它承担起了在开发环境模块热加载本地服务接口代理等非常重要的功能。

本文是笔者对wds的一些理解和认识,希望在项目中有所帮助。

正文开始...

在阅读本文之前,本文会大概从下几个方面去了解wds

1、了解wds是什么

2、wds在 webpack 中如何使用

3、项目中使用wds是怎么样的

4、关于配置devServer的一些常用配置,代理等

5、wds如何实现模块热加载原理

了解webpack-dev-server

顾名思义,这是一个在开发环境下的使用的本地服务,它承担了一个提供前端静态服务的作用

首先我们快速搭建一个项目,创建一个项目webpack-07-wds执行npm init -y,然后安装基础支持的插件

npm i webpack webpack-cli html-webpack-plugin webpack-dev-server -D

创建一个webpack.config.js

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js'
  },
  plugins: [
    new htmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
};

在根目录下创建public,新建html文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack-for-dev-server</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

我们在入口文件写入一段简单的代码

// index
(() => {
  const appDom = document.getElementById('app');
  appDom.innerHTML = 'hello webpack for wds';
})();

我们已经准备好了内容,现在需要启动wds,因此我们需要在在package.json中启动服务

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack server"
  },

执行npm run start

万事大吉,原来就是一行命令就可以了

但是这行命令的背后实际上有webpack-cli帮我们做了一下事情,实际上在.bin目录下,当你执行该命令时,webpack就会启用告知webpack-dev-server开启服务,通过webpack根据webpack.config.js的配置信息进行compiler,然后再交给webpack-dev-server处理

参考官方文档webpack-dev-serveropen in new window

根目录新建server.js

// server.js
const webpack = require('webpack');
const webpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.config.js');
// webpack处理入口配置相关文件
const compiler = webpack(webpackConfig);
// devServer的相关配置
const devServerOption = {
  port: 8081,
  static: {
    directory: path.join(__dirname, 'public')
  },
  compress: true // 开启gizps压缩public中的html
};
const server = new webpackDevServer(devServerOption, compiler);
const startServer = async () => {
  console.log('server is start');
  await server.start();
};
startServer();

终端执行node server.js或者在package.json中配置,执行npm run server

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack server",
    "server": "node ./server.js"
  }

打开页面http://localhost:8081地址,发现是ok

我们注意到可以使用webpack server启动服务,这个主要是webpack-cli的命令serveropen in new window

关于在命令行中设置对应的环境,在以前项目中可能你会看到,process.env.NODE_ENV这样的设置

你可以在cli命令中配置,注意只能在最前面设置,不能像以下这种方式设置webpack server NODE_ENV=test NODE_API=api,不然会无效

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "NODE_ENV=test NODE_API=api webpack server",
    "server": "node ./server.js"
  },

webpack.config.js中就可以看到设置的参数

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

console.log(process.env.NODE_ENV, process.env.NODE_API); // test api
module.exports = {
  entry: './src/index.js',
  mode: 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js'
  },
  plugins: [
    new htmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
};

你可以设置--node-env xxx环境参数来指定环境变量

 "start:test": "webpack server --node-env test",

更多参数设置参考官方cliopen in new window

wds 在 webpack 中的使用

我们上述是用一个server.js,通过命令行方式,调用webpack-dev-serverAPI 方式去启动一个本地的静态服务,但是实际上,在webpack中直接在配置devServeropen in new window接口中配置就行。

了解几个常用的配置

  • port 指定端口打开页面
  • client
    • overlay 当程序错误时,浏览器页面全屏警告
    • webSocketURL 允许指定 websocket 服务器
  • progress 启动开发环境 gizp 压缩静态 html
  • historyApiFallback 当使用路由模式为 history 时,必须设置这个,要不然前端刷新直接 404 页面
  • hot模块热加载,需要结合module.hot.accept('xxx/xxx')指定某个模块热加载module.hot.acceptopen in new window
  • open 当我们启动本地服务时,自动打开指定配置端口的浏览器
module.exports = {
  ...
  devServer: {
      port: '9000',
      client: {
      progress: true, // 启用gizp
      overlay: {
        errors: true, // 如果有错误会有蒙层
        warnings: false,
      },
      webSocketURL: {
        hostname: '0.0.0.0',
        pathname: '/ws',
        port: 8080,
        protocol: 'ws',
      }
    },
    historyApiFallback: true, // 使用路由模式为history时,必须设置这个,要不然前端刷新会直接404页面
    hot: true, // 模块热加载,对应模块须配合module.hot.accept('xxx/xxx.js')指定模块热加载
    open: true, // 当服务启动时默认自动直接打开浏览器,可以指定打开哪个页面
  }
}

proxy

proxy 这是项目中接触最多一点,也是初学者配置代理时常最令人头疼的事情,实际上proxy本质就是将本地的接口路由前缀代理到目标服务器环境,可以同时代理多个不同环境,具体参考以下

...
module.exports = {
  ...
  devServer: {
    ...
    proxy: {
      '/j': {
        target: 'https://movie.douban.com', // 代理豆瓣
        changeOrigin: true
      },
      '/cmc': {
        target: 'https://apps.game.qq.com', // 代理王者荣耀官网
        changeOrigin: true, // 必须要加,否则代理接口直接返回html
        pathRewrite: { '^/cmc': '/cmc' },
      }
    }
  }
}

我们修改index.js

(() => {
  const $ = (id) => document.getElementById(id);
  const appDomMovie = $('movie');
  const gameDom = $('wang');
  // appDom.innerHTML = 'hello webpack for wds,';
  // https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=%E7%94%B5%E5%BD%B1&start=0
  // 豆瓣电影
  const featchMovie = async () => {
    const { data = [] } = await (await fetch('/j/new_search_subjects?sort=U&range=0,10&tags=%E7%94%B5%E5%BD%B1&start=0')).json();
    // console.log(data)
    const divDom = document.createElement('div');
    let str = '';
    data.forEach((item) => {
      const { title, rate } = item;
      str += ` <span>${title},${rate}</span>`;
    });
    divDom.innerHTML = str;
    appDomMovie.appendChild(divDom);
  };
  featchMovie();
  const wangzherongyao = async () => {
    const divDom = document.createElement('div');
    // https://apps.game.qq.com/cmc/cross?serviceId=18&filter=tag&sortby=sIdxTime&source=web_pc&limit=20&logic=or&typeids=1%2C2&exclusiveChannel=4&exclusiveChannelSign=8a28b7e82d30142c1a986bb7acdcc068&time=1655732988&tagids=931
    // 王者荣耀官网
    const {
      data: { items = [] }
    } = await (
      await fetch(
        '/cmc/cross?serviceId=18&filter=tag&sortby=sIdxTime&source=web_pc&limit=20&logic=or&typeids=1%2C2&exclusiveChannel=4&exclusiveChannelSign=8a28b7e82d30142c1a986bb7acdcc068&time=1655732988&tagids=931'
      )
    ).json();
    let str = '';
    console.log(items);
    items.forEach((item) => {
      const { sTitle, sIMG } = item;
      str += `<div>
          <img src=${sIMG} />
          <div>${sTitle}</div>
      </div>`;
    });
    divDom.innerHTML = str;
    gameDom.appendChild(divDom);
  };
  wangzherongyao();
})();

对应的两个接口数据就已经在页面上渲染出来了

对于代理我们会常常容易会犯以下几个误区

  • 第一种, 多个接口代理,第一个直接以/代理,这会造成第二个代理无效,接口直接 404,优先级会先匹配第一个
{
 devServer: {
   proxy: {
      '/': {
        target: 'https://movie.douban.com', // 代理豆瓣
        changeOrigin: true,
      },
      '/cmc': {
        target: 'https://apps.game.qq.com', // 代理王者荣耀官网
        changeOrigin: true, // 必须要加,否则代理接口直接返回html
        pathRewrite: { '^/cmc': '/cmc' },
      }
    }
 }
}
  • 第二种,pathRewrite要不要加,什么时候该加,不知道你发现没有我第一个接口拦截并没有加pathRewrite,但是和第二个加了效果是一样的。

现在有一个场景,就是你本地测试服务接口与线上接口是有区别的,一般你在本地开发是联调环境,后端的接口不按照常理出牌,假设联调环境后端就是死活不同意统一接口路径怎么办?

现在假设后端接口

联调环境:/dev/api/cmc/cross

线上环境是/api/cmc/cross

于是你想到有以下两种方案:

1、在 axios 请求拦截根据环境变量手动添加前缀,但是这不是一种很好的方案,相当于把不确定性的逻辑代码打包到线上去了,有一定风险

2、不管开发环境还是本地联调环境都是统一的路径,仅仅只是在proxypathRewrite做处理,这样风险很小,不容易造成线上接口 404 风险

于是这时候pathRewrite的作用就来了,重写路径,注意是pathRewrite: { '^/cmc': '/dev/cmc' }

我们仅仅是在开发环境重新了/cmc接口路径,实际上代码环境的代码并不会打包到线上

  {
 devServer: {
   proxy: {
      '/j': {
        target: 'https://movie.douban.com', // 代理豆瓣
        changeOrigin: true,
      },
      '/cmc': {
        target: 'https://apps.game.qq.com', // 代理王者荣耀官网
        changeOrigin: true, // 必须要加,否则代理接口直接返回html
        pathRewrite: { '^/cmc': '/dev/cmc' },
      }
    }
 }
}
  • 第三种,缺少changeOrigin:true,像下面这种丢失了changeOrigin是不行的
 devServer: {
   proxy: {
      '/j': {
        target: 'https://movie.douban.com', // 代理豆瓣
        // changeOrigin: true,
        pathRewrite: { '^/j': '/j' },
      },
      '/cmc': {
        target: 'https://apps.game.qq.com', // 代理王者荣耀官网
        //changeOrigin: true,
        pathRewrite: { '^/cmc': '/dev/cmc' },
      }
    }
 }
}

如果遇到有多个路由指向的是同一个服务器怎么办,别急,官网有方案,你可以这么做

{
  devServer: {
    proxy: [
      {
        context: ['/j', '/cmc'],
        target: 'https://movie.douban.com'
      }
    ];
  }
}

项目常用的就是以上这些了,另外拓展的,比如可以支持本地https,因为默认本地是http,还有支持当前可以开启一个websocket服务,更多配置参考官网,或者有更多特别的需求,及时翻阅官网open in new window

WDS 模块热加载原理(HMR)

只更新页面模块变化的内容,无需全站刷新

本质上就是webpack-dev-server中的两个服务,一个express提供的静态服务,通过webpackcompiler入口的依赖文件,加载打包内存中的bundle.js

第二个模块热加载是一个websocket服务,通过socketio,当源码静态文件发生变化时,此时会生成一个manifest文件,这个文件会记录一个hash以及对应文件修改的chunk.js,当文件修改时websocket会单独向浏览器发送一个ws服务,从而更新页面部分模块,更多可以参考官网hot-module-replacementopen in new window

总结

  • 了解webpack-dev-server是什么,它是一个开发环境的静态服务

  • webpack-dev-server在 webpack 中的使用

  • 关于WDS一些常用的配置,比如如何配置接口代理等

  • 浅识HMR模块热加载,原生webpack虽然也提供了模块热加载,但是webpack-dev-server可以实现模块热加载,常用框架,比如vue,内部热加载是用vue-loader实现的,在使用WDS时,默认是开启了热加载的。

扫二维码,关注公众号
专注前端技术,分享web技术
加作者微信
扫二维码 备注 【加群】
教你聊天恋爱,助力脱单
微信扫小程序二维码,助你寻他/她
专注前端技术,分享Web技术
微信扫小程序二维码,一起学习,一起进步
前端面试大全
海量前端面试经典题,助力前端面试