1

webpack的devServer引发的思考

前端 1 862 0
发表于: 2022-04-25 04:07:05

简介: devServer并不只有热更新和代理

起因

大多数人对devServer的认识,可能就只是在开发模式时可以热更新,它在构建生产环境的时候不会用到devServer。但devServer的作用远不止于此,它其实是可以模拟线上部署的环境的,虽然,前端能做的就只是将代码打包成静态文件然后丢给后端部署,然后就没有前端的事情了,但是有的时候,并不是前端构建完就拍拍屁股走人了,如果后端部署项目部署的有问题,有的时候必须得前端改,因此,如果真正理解了devServer,基本遇到了这种问题,是一定(至少一般情况下)能定位到是前端自己的问题还是后端的问题的,而且即使是前端的问题,也能知道如何解决,这点非常的重要,不然的话就只能听后端或者别人说什么就是什么了,而且他们说的不一定对(有可能是他们自己也不懂,先甩的锅),自己一点办法没有,只能往上一通的百度谷歌,最后可能随便cv了一段代码就解决了,其实这样是没什么意义的。了解真相才能获得真正的自由(by codewhy)。

正文

本文基于的环境是webpack5,并不基于vuecli或者create-react-app这种开箱即用的脚手架。举例用的前端主要技术栈是vue3.x+vue-router4.x,webpack配置的入口只有一个(即最终构建出来的是单页面应用)

这里我说下我的理解,如有错误欢迎指正,

首先,我们webpack serve的时候,其实做的就是一件事情,找入口(entry),然后根据入口的文件进行编译构建,最终编译的结果,会被webpack-dev-server 托管起来,从流程来看,可以看出来这里涉及的并东西不仅仅是devServer这个配置,还有entry配置,但是除了这两个配置,其实还涉及到output配置,下面用几个场景来说明下,为什么devServer会关联到它们

output.publicPath

webpack-dev-server 也会默认从 output.publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。

所以不管开发模式还是生产模式,output.publicPath都会生效,

output的publicPath建议(或者绝大部分情况下必须)与devServer的publicPath一致。

如果不设置publicPath,它默认就约等于output.publicPath:“”,到时候不管开发还是生产模式,最终引入到

index.html的所有资源都会拼上这个路径,如果不设置output.publicPath,会有问题:

比如vue的history模式下,如果不设置output.publicPath,如果路由全都是/foo,/bar,/baz这样的一级路由没有问题,

因为引入的资源都是js/bundle.js,css/bundle.css等等,浏览器输入:http://localhost:8080/foo,回车访问,

引入的资源就是http://localhost:8080/js/bundle.js,http://localhost:8080/css/bundle.css,这些资源都

是在http://localhost:8080/根目录下的没问题,但是如果有这些路由:/logManage/logList,/logManage/logList/editLog,

等等超过一级的路由,就会有问题,因为没有设置output.publicPath,所以它默认就是"",此时浏览器输入:

http://localhost:8080/logManage/logList回车访问,引入的资源就是http://localhost:8080/logManage/logList/js/bundle.js,

而很明显,我们的http://localhost:8080/logManage/logList/js目录下没有bundle.js这个资源(至少默认情况下是没有,除非设置了其他属性)

找不到这个资源就会报错,这种情况的路由是很常见的,所以建议默认必须手动设置output.publicPath:“/”,这样的话,

访问http://localhost:8080/logManage/logList,引入的资源就是:http://localhost:8080/js/bundle.js,就不会报错。

此外,output.publicPath还可设置cdn地址。

案例1

我们webpack主要配置如下:

entry: {
  main: {
    import: './src/main.ts',
  },
},
output: {
  publicPath: '/',
},
devServer: {
  hot: true, // 启用 webpack 的热模块替换功能
  compress: true, // 为所有服务启用gzip 压缩
  port: 8080, // 开发服务器端口,默认8080
},

我们的vue-router配置如下:

const router = createRouter({
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('@/views/home/index.vue'),//这个里面可以跳转about页面和bar页面
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('@/views/about/index.vue'),
    },
    {
      path: '/bar',
      name: 'bar',
      component: () => import('@/views/bar/index.vue'),
    },
  ],
  history: createWebHistory(),
});

从vue-router可以看出来,前端路由模式用的是history模式(地址栏不带#),而不是hash默认(地址栏带#),然后有三个页面,一个是首页home,一个是about,最后一个是bar

此时我们webpack serve后,会在本地的8080端口开启一个服务,并且打开了home页面,我们点击home页面的切换about和bar按钮,也能正常的跳转页面,此时看起来并没有什么异样,但是我们如果在localhost:8080/about或者localhost:8080/bar点击浏览器的刷新按钮(或者在地址栏直接回车),就会出现:Cannot GET /about或者Cannot GET /bar,但是我们在localhost:8080/里直接刷新或者地址栏回车,却不会出现Cannot GET /,还是能正常的展示页面,这是为什么?此时就得知道前端路由和后端路由了。

首先在这个场景里面,我们的前端路由毫无疑问就是vue-router控制的,我们通过点击页面的跳转,不会出现Cannot GET /about这种错误,说明问题不是出在前端路由,因此问题是在后端路由上面,但是后端路由是由谁控制的?很明显,是由webpack的devServer控制的,devServer其实底层是用了express作为服务器(如果你接触过express,看到Cannot GET /about这种提示,你应该不会感觉到陌生),为了篇幅原因,这里就不说express的使用了,长话短说的就是express可以作为后端提供api接口,如果请求一个不存在的接口时,就会报类似Cannot GET /xxx这些错误,因此,我们在localhost:8080/about刷新的时候,实际上是浏览器发起了请求,去请求localhost:8080/about的资源,默认情况下,其实是会找localhost:8080/about里面的index.html的,如果找不到的话,其实是应该报404的,但是我们这里是报Cannot GET /about,是因为这其实是express返回的,所以没报404而报了Cannot GET /about。

那么这个问题该如何解决?其实有两个办法,第一个办法就是将webpack改成多入口,这样的话就会每一个路由都有自己的index.html,但是大多数前端的应用其实还是单页面应用,因此采用第二个办法,就是重定向,让express匹配到404后,直接让express返回localhost:8080/里的index.html,这样的话,请求返回的页面其实本质上的页面还是同一个页面,但是地址栏却变成了localhost:8080/about,就还会触发前端路由的逻辑,这样就可以解决问题了,那么该怎么配置才能做到这个效果,其实只需要在devServer加上一行配置即可:

devServer: {
  hot: true, // 启用 webpack 的热模块替换功能
  compress: true, // 为所有服务启用gzip 压缩
  port: 8080, // 开发服务器端口,默认8080
  historyApiFallback: true,//加上这个配置之后,404的请求都会重新匹配到index.html
},

加上配置后重新webpack serve,再次跳转到localhost:8080/about刷新,发现不会出现Cannot GET /about,而且也能正常的展示about的内容了,所以你以为这个问题就此解决了是吗?其实对于前端来说,其实可以不用解决,因为devServer的配置在构建生产模式的时候,并不会用到,那我们花时间去解决这个问题有啥用???其实它真正的用处在于,你的项目部署到了线上,但是线上的时候出现了刷新404的问题,后端或者运维找到你了,你能知道问题所在,而且知道该怎么告诉后端/运维,帮助他们解决问题(其实某种程度上这也是自己力所能及的一部分)。我们本地调试因为加了devServer的配置所以解决了刷新404的问题,但是线上的时候改如何解决,那么首先得和他们确认两个问题,一个是用什么托管自己的构建好的静态资源,另一个是托管在服务器的那个位置。

托管在根目录

这里我用我自己的项目做举例,我使用nginx进行托管:

server {
    listen 80;
    server_name project.hsslive.cn;

    location / {
        root /node/;
        index index.html index.htm;
    		try_files $uri $uri/ /index.html;#加上它可以解决404
    }

}

不了解nginx的小伙伴可以自己学习下,这里大概讲下上面的nginx配置是什么意思,定义了一个匹配规则/,如果匹配到/,就会找到服务器的node目录,默认找node目录里的index.html。

此时只需要加上一句 try_files $uri $uri/ /index.html; 即可,$uri其实是和你地址栏输入路径有关,比如地址栏输入了project.hsslive.cn/about,$uri就是/about,地址栏输入:project.hsslive.cn/bar,$uri就是/bar。整句try_files $uri $uri/ /index.html; 的意思就是请求project.hsslive.cn/about时,会依次查找:

  1. $uri,即project.hsslive.cn/about,即先会找node目录里面有没有about这个文件,如果没有就继续找下一个
  2. $uri/,即project.hsslive.cn/about/目录,找node目录的about目录,看看里面有没有index.html,没有的话就继续找下一个
  3. /index.html,即project.hsslive.cn/index.html,找node目录里面的index.html,而/node/里面是我们构建好的静态文件,肯定是有index.html文件的,因此,就会返回这个index.html,至此,这404的这个问题就能得到解决

托管在非根目录

server {
    listen 80;
    server_name project.hsslive.cn;

    location /aaa/ {
        root /node/;
        index index.html index.htm;
    }

}