1

webpack的devServer引发的思考

前端 1 1175 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;
    }

}

如果我们的项目并不是托管在服务器的根目录(project.hsslive.cn)我们托管在aaa这个目录里,那么又该怎么处理?,此时就不是只需要加上一句 try_files $uri $uri/ /index.html; 的问题了,这样要改的东西就多了点,首先,得再vue-router里面的history配置改成这样

const router = createRouter({
	//...
  history: createWebHistory('/aaa/'),
});

然后,webpack的output.publicPath改成:

output: {
  publicPath: '/aaa/',
},

上面改的东西是前端要改的,目的是保证前端路由跳转的时候没问题,以及构建出来的资源没有问题,两者都得带上子目录。

然后nginx的配置要改成这样:

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

    location /aaa/ {
        #root /node/; #不能这样了,如果这样的话,/aaa/时,实际访问的是node目录里面的aaa目录。
        alias /node/; #这样的话,访问/aaa/时,实际访问的就是/node/目录
        index index.html index.htm;
        try_files $uri $uri/ /index.html;#加上它可以解决404
    }

}

devServer如何模拟线上

其实不难看出来,devServer做的事情其实就是部署到线上之后,后端/运维他们要做的事情,后端用什么托管我们的静态文件其实和我们无法改变,但是后端部署到根目录和非根目录,其实是和我们前端有关系的,那么我们是否可以利用devServer,在本地开发的时候就模拟部署到线上的情况呢,答案当然是可以的,从上面的案例可以看出来,部署到哪个目录,其实和前端路由以及webpack的output.publicPath有关,后端部署到/aaa/,我们的前端路由和webpack的output.publicPath就得加上/aaa/,因此,我们可以将这两个东西抽成一个变量进行引用,这样就可以做到根据部署到哪个目录就拼上哪个目录了。此外还有一点很重要,因为除了这前端路由和output.publicPath以外,还有刷新404也得解决,部署到线上是后端解决,但是我们本地模拟的话,我们本地就要解决,devServer当然也能做这件事情,还是之前的historyApiFallback配置,但是并不是直接historyApiFallback:true就可以了,因为historyApiFallback:true只适用于托管在根目录的情况,如果托管在非跟目录,就得换一个写法,比如我们托管到/abc/目录下,就这样写即可:

historyApiFallback: {
  rewrites: [
    { from: /abc/, to: /abc/ },
  ],
},

案例2

devServer还有一个很重要的作用就是proxy,proxy即代理,意思就是会将我们的请求进行转发代理,因为我们浏览器有同源策略,不能从localhost:8080请求其他非同源的接口,通俗的讲,就是我们本地开发是在localhost:8080,那么我们就只能请求从localhost:8080/xxx/之类的接口,不能请求非localhost:8080的接口,我们希望请求线上的接口,比如:http://42.193.157.44:3300/article/find/1 ,我们在localhost:8080直接发起http://42.193.157.44:3300/article/find/1请求,是会报跨域问题的,我们可以在devServer添加proxy配置,然后修改我们的请求地址为:/api/article/find/1,这样就可以请求到http://42.193.157.44:3300/article/find/1的数据了,虽然没看过源码,但其实原理应该很简单,因为devServer是基于express的,express能托管我们的构建的静态资源,也可以提供api接口,而且,express运行在node环境,node环境是不受浏览器的同源策略影响的,node里面可以直接请求http://42.193.157.44:3300/article/find/1接口拿到数据,然后再将这些数据原封不动的返回给我们,devServer的proxy就很顾名思义了,就只是做了一层转发,监听到把我们的/api/xxx请求,就将/api替换成http://42.193.157.44:3300/xxx,然后发起请求,把请求到的http://42.193.157.44:3300/xxx数据,通过/api/xx接口再返回我们。

devServer: {
  //....
  proxy: {
    '/api': {
      target: 'http://localhost:3300',
      secure: false, // 默认情况下(secure: true),不接受在HTTPS上运行的带有无效证书的后端服务器。设置secure: false后,后端服务器的HTTPS有无效证书也可运行
      /**
       * changeOrigin,是否修改请求地址的源
       * 默认changeOrigin: false,即发请求即使用devServer的localhost:port发起的,如果后端服务器有校验源,就会有问题
       * 设置changeOrigin: true,就会修改发起请求的源,将原本的localhost:port修改为target,这样就可以通过后端服务器对源的校验
       */
      changeOrigin: true,
      pathRewrite: {
        // '^/admin': '', // 效果:/api/link/list ==> http://localhost:3300/link/list
        '^/api': '/admin/', // 效果:/api/link/list ==> http://localhost:3300/admin/link/list
      },
    },
}

devServer配置

最后附上相对完整的配置,如果想看比较系统的配置,可以看本文结尾的完整实现

devServer: {
      client: {
        logging: 'none', // https://webpack.js.org/configuration/dev-server/#devserverclient
      },
      hot: true, // 启用 webpack 的热模块替换功能
      // hot: 'only', // 要在构建失败的情况下启用热模块替换而不刷新页面作为后备,请使用hot: 'only'。但在vue项目的话,使用only会导致ts文件没有热更,得使用true
      compress: true, // 为所有服务启用gzip 压缩
      port, // 开发服务器端口,默认8080
      open: false, //告诉 dev-server 在服务器启动后打开浏览器。
      historyApiFallback: {
        rewrites: [
          /**
           * 如果publicPath设置了/abc,就不能直接设置historyApiFallback: true,这样会重定向到根目录下的index.html
           * publicPath设置了/abc,就重定向到/abc,这样就可以了
           */
          { from: /abc/, to: /abc/ },
        ],
      },
      /**
       * devServer.static提供静态文件服务器,默认是 'public' 文件夹。static: false禁用
       * 即访问localhost:8080/a.js,其实访问的是localhost:8080/public/a.js
       * 因为CopyWebpackPlugin插件会复制public的文件,所以static: false后再访问localhost:8080/a.js,其实还是能访问到public目录的a.js
       */
      static: {
        watch: true, //告诉 dev-server 监听文件。默认启用,文件更改将触发整个页面重新加载。可以通过将 watch 设置为 false 禁用。
        publicPath: outputStaticUrl(false),
      },
      proxy: {
        '/api': {
          target: 'http://localhost:3300',
          secure: false, // 默认情况下(secure: true),不接受在HTTPS上运行的带有无效证书的后端服务器。设置secure: false后,后端服务器的HTTPS有无效证书也可运行
          /**
           * changeOrigin,是否修改请求地址的源
           * 默认changeOrigin: false,即发请求即使用devServer的localhost:port发起的,如果后端服务器有校验源,就会有问题
           * 设置changeOrigin: true,就会修改发起请求的源,将原本的localhost:port修改为target,这样就可以通过后端服务器对源的校验
           */
          changeOrigin: true,
          pathRewrite: {
            // '^/admin': '', // 效果:/api/link/list ==> http://localhost:3300/link/list
            '^/api': '/admin/', // 效果:/api/link/list ==> http://localhost:3300/admin/link/list
          },
        },
        '/prodapi': {
          target: 'http://42.193.157.44:3200',
          secure: false,
          changeOrigin: true,
          pathRewrite: {
            '^/prodapi': '/admin/',
          },
        },
        '/betaapi': {
          target: 'http://42.193.157.44:3300',
          secure: false,
          changeOrigin: true,
          pathRewrite: {
            '^/betaapi': '/admin/',
          },
        },
      },
    },

完整实现

https://github.com/galaxy-s10/vue3-blog-admin

webpack

最后更新于:2022-06-06 17:39:19

欢迎评论留言~
0/400
支持markdown
Comments | 1 条留言
按时间
按热度
- 管理员 超级管理员
2 年前

脑子有百分百的想法,但是只写了百分之三四十的内容

0 点赞
回复
广东省 - 广州市 undefined undefined
已加载所有留言~