增强开发体验

前端工程化模块化开发Webpack

# webpack开发体验问题

编写源代码 -> webpack打包 -> 运行应用 -> 刷新浏览器

上面周而复始的方式过于原始,实际开发中还这样使用就会降低开发效率。

设想:理想的开发环境

  1. 使用HTTP SERVER运行
  2. 自动编译 + 自动刷新
  3. 提供 Source Map支持(运行过程中有错误信息,就可以快速定位到源代码的位置,便于调试应用)

# 自动化

# 实现自动编译

webpack-cli 提供的watch工作模式——监听文件变化,自动重新打包。

webpack --watch

# 编译过后自动刷新浏览器

# BrowserSync

  1. 安装npm i browser-sync -g
  2. 在一个命令行执行webpack --watch
  3. 在另一个命令行中执行browser-sync dist --files "**/*" (同时去监听dist目录下的文件变化)
  4. 完美~

依旧存在的问题:

  • 操作太麻烦
  • 效率太低,这个过程中webpack不停的将文件写入磁盘,browsers-sync又将文件从磁盘读出,多了两步的磁盘读写操作

# Webpack Dev Server

是webpack官方推出的一个开发工具,它提供了用于开发的HTTP Server,并且集成【自动编译】和【自动刷新浏览器】等功能。

因为是高度集成,所以使用也简单

  1. 安装npm i webpack-dev-server --save-dev

ps: 这里启动webpack-dev-server对webpack-cli的版本有要求,目前安装了webpack-cli的版本是3.3.12,是工作的,4.1.0的版本无法启动服务,详情见 webpack-dev-server报错处理 (opens new window)

  1. 启动服务
  • 使用yarn安装的可以直接在命令行中写yarn webpack-dev-server
  • 使用npm安装到全局的可以直接使用webpack-dev-server命令启动服务
  • 在package.json的script里面写
"scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server"
}
1
2
3
4

然后在命令行中写npm run dev也可以启动服务

修改任意文件都可以做到热更新

  1. 还可以在后面添加--open的参数,自动唤起浏览器打开运行地址

# 工作原理

webpack-dev-server为了提高工作效率,所以并没有将打包结果写入磁盘当中,是将打包结果暂时存放在内存中,其内部http server也是从内存中把文件读出来并发送给浏览器,这样就可以减少很多不必要的磁盘读写操作,从而大大提高我们的构建效率

# 静态资源访问

默认会将构建结果输出的文件全部作为开发服务器的资源文件,只要是webpack打包能够输出的文件,都可以正常被访问到,那其他静态资源文件也需要serve,就需要额外的告诉webpack-dev-server

// 在webpack.config.js中有一个devServer属性,这个是专门为webpack-dev-server设置的配置选项。
devServer: {
    // contentBase是额外为开发服务器指定查找资源目录的选项
    // 这个属性可以是一个字符串也可以是一个数组,我们可以配置一个或者多个路径
    contentBase: './public'
}
1
2
3
4
5
6

这样访问127.0.0.8080/favicon.ico的时候访问的就是contentBase里面设置的public目录

# 代理API

我们在本地开发,如果请求API接口,会有跨域问题。

如何解决?

  • 可以使用跨域资源共享(CORS),但是并不是任何情况下API都支持,如果前后端同源部署(协议、域名、端口都一致)我们没有必要去开启CORS
  • 在本地开发服务器上配置代理服务,webpack支持配置代理服务
  1. 以我们要在本地访问https://api.github.com/users的接口为例,先在webpack.config.js中修改
devServer: {
    // 这个属性可以是一个字符串也可以是一个数组,我们可以配置一个或者多个路径
    contentBase: './public',
    // 添加代理服务配置,是个对象,每个属性就是一个代理规则的配置
    proxy: {
      // 键是请求路径前缀,那个地址开始就会走代理请求,值是代理规则配置
      '/api': {
        // 代理目标
        target:'https://api.github.com',
        /**
         * 我们请求http://localhost:8080/api/users =>相当于请求了 https://api.github.com/api/users
         * 
         * 但是我们实际上请求的是https://api.github.com/users,并没有/api,所以我们需要去掉/api
         */
        // 代理路径进行重写
        pathRewrite: {
          // 正则的方式,以api为开头
          '^/api': ''
        },
        // 我们用本地浏览器请求gitHub的服务器默认会用 localhost:8080 作为主机名,但是 GitHub 会根据主机名进行识别,所以需要修改。true就是用原有代理的状态去请求
        changeOrigin: true
      }
    }
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. http://localhost:8081/api/users就相当于请求了https://api.github.com/users的接口。

  2. 在main.js中编写代码

// ======================== fetch proxy api example ========================

const ul = document.createElement('ul')
document.body.append(ul)

// 跨域请求,虽然 GitHub 支持 CORS,但是不是每个服务端都应该支持。
// fetch('https://api.github.com/users')
fetch('/api/users') // http://localhost:8080/api/users
  .then(res => res.json())
  .then(data => {
    data.forEach(item => {
      const li = document.createElement('li')
      li.textContent = item.login
      ul.append(li)
    })
  })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

可以看到数据全部展示出来了,大功告成。

# Source Map(源代码地图)

# Source Map出现原因

通过构建编译之类的操作可以在开发阶段的源代码转化为能够在生产环境中运行的代码,这是一种进步。而我们在实际写的代码和生产环境中运行的代码有很大的差异,在这种情况下如果要调试应用,或者在运行应用的过程中出现错误,我们就无法定位

Source Map就是解决这类问题的最好的办法。

# Source Map作用

它的作用就是用来映射我们转换过后的代码和源代码之间的关系。一段转换后的代码,我们通过转换过程中生成的Souce Map文件就可以逆向得到源代码。

现在很多第三方库在发布的文件中都有.map后缀的SourceMap文件

  • version —— 当前这个文件使用的Source Map的标准)
  • sources —— 转换之前源文件的名称,因为有可能是多个文件转化成一个文件,所以这里的值是数组类型
  • names —— 源代码中使用的一些成员名称,压缩的时候会更换变量名,这里是原始变量对应的名称
  • mappings —— 整个source map的核心属性,base64_VLQ编码的字符串,记录的信息是转换过后代码字符与转换之前对应的映射关系,有了这个代码,我们会在转换过后的代码中通过添加注释的方式去引入source map文件

只是帮助开发者更容易去调试和定位错误的,所以对生产环境没有太大的意义。

# Source Map基本使用

  1. 在js文件最后添加注释
# sourceMappingURL=jquery-3.4.1.min.map
1
  1. 在浏览器中使用开发者工具打开,它阅读到这个注释之后就会自动请求Source Map文件,根据文件内容逆向解析出源代码便于调试。

  2. 在控制台的sources可以看到这里也请求了本身没有压缩过的代码,可以直接在上面打断点调试

# 在webapck中配置Source Map

webpack支持对我们的打包结果生成Source Map文件,其提供了很多不同的模式。

  1. 在webpack.config.js中进行配置
//开发中的辅助工具,也就是与Source Map相关的配置
devtool: 'source-map'
1
2
  1. 命令行运行npm run build可以看到在output目录下有一个bundle.js.map的文件。运行npm run dev启动服务就可以直接使用Source Map了

# Source Map各种模式对比

webpack支持12种不同的方式,每种方式的效率和效果各不相同。效果最好的一般生成速度也就最慢,速度最快的效果一般,哪种最好需要探索。

source map各种模式

  • build —— 初始构建速度
  • rebuild —— 监视模式重新打包速度
  • production —— 是否适合在生产环境中使用
  • quality —— 生成的source map的质量

# eval模式

eval是js中的函数,可以去运行js代码eval('console.log(123)'),这个可以指定其运行的环境

image

这样我们就可以指定代码的运行环境。

  1. 在webpack.config.js中指定devtool为eval模式
devtool: 'eval'
1
  1. 打包之后再浏览器中运行,看到的错误显示是打包过后的代码

image

其原理是将所有的模块代码都放在eval中执行,并且在代码最后用sourceURL指明对应的路径。

eval("__webpack_require__.r(__webpack_exports__); ... const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://fast/./src/main.css?./node_modules/css-loader/dist/cjs.js");
1

所以浏览器在执行的时候知道所属的文件,这个模式不会生成source map文件,所以是执行速度最快的,但是其只能定位文件名称,不知道行列信息。

# 准备工作

  1. 新起一个项目 devtool-diff的项目 (opens new window), 注意在安装的时候要用webpack4,5里面对devtool的名称进行了修改,所以有些名称可能找不到
  • src
    • main.js
    • heading.js
  • package.json
  • webpack.config.js
  1. 运行npm run build可以看到生成了一个dist目录,里面是生成的html文件
  2. 安装npm i http-server --save-dev,并且在package.json里面添加
"scripts": {
    "build": "webpack",
    "serve": "http-server"
}
1
2
3
4

之后在命令行中写npm run serve dist/就可以在浏览器中看到文件内容

# eval-source-map

eval函数执行代码,这里除了定位文件,也可以定位到行和列的信息,想比与eval它生成了source map文件。

# cheap-eval-source-map

阉割版的source map,只能定位到某个文件的行,少了列的信息。少了效果的话生成速度就会快很多。是经过ES6转换过后的结果。

# cheap-module-eval-source-map

只能定位到行,与cheap-eval-source-map大体相同,唯一不同是未经过ES6的转化,与源代码一样。

# cheap-source-map

没有eval就是没有用eval函数的方式执行代码,没有module就是loader处理之后的代码

# inline-source-map

source map一般是以文件的形式存在,inline是dataURL的方式嵌入代码中。eval-source-map也是用行内的方式嵌入进去的。

ps: 这种方式是最不可能用到的,用dataURL的方式嵌入后,代码的体积会大很多。

# hidden-source-map

我们在开发模式下是看不到source map的效果的。但是在开发工具中,他确实生成了source map文件。 这个和jquery是一样的,在构建过程中生成了这个文件,但是在代码中并没有通过注释的方式引入这个文件。所以我们在开发工具中看不到效果。

ps: 这个比较适合第三方的库去使用,我们生成了source map但是我们不使用,当别人引入的时候出现了错误,他可以手动引用source map调试。

# nosources-source-map

这个模式下我们能看到错误出现的位置,但是调试工具中我们点进去是看不到源代码的。根据信息我们可以在源代码中找到对应的工具。

这是为了在生产环境中,我们的源代码不会被暴露的情况。

# 特点总结

  • eval:是否使用eval执行模块代码,有就是用eval函数处理,没有就是没有用eval函数处理
  • cheap:source map是否包含列的信息,带的不含列信息,不带含列信息
  • module:是否能够得到Loader处理之前的源代码,有就是没用loader处理,没有就是用loader处理过
  • inline
  • hidden
  • nosources

# 选择合适的Source Map

经验之谈:

开发环境下 生产模式下
cheap-module-eval-source-map none
1. 每行不超过80个字符,定位到行即可
2. 经过Loader转换过后的代码差异较大,我们调试转换之前的即可。
3. 首次打包速度慢无所谓,我们用webpack监听重写打包相对较快即可。
1. Source Map会暴露源代码
2. 调试是开发阶段的事情,如果没有信心的话可以使用nosources-source-map模式,这样可以找到位置但是不至于暴露源代码的内容

上面的选择不绝对,理解不同模式的差异,就是为了适配不同的环境。

# HMR —— Hot Module Replacement

# 背景介绍

webpack自动刷新带来的问题: 我们要是在浏览器中调试文本等,一旦编译器修改样式,浏览器刷新调试的文字就丢失了,如果不满意继续修改,还会丢失,很不方便。

野方法有两个:

  1. 代码中写死编辑器的内容
  2. 额外代码实现刷新前保存,刷新后读取

上面的方法都不是很好,让我们在项目中编写了一些和业务无关的代码,也没有解决问题的核心 —— 自动刷新导致的页面状态丢失

解决的方向: 在页面不刷新的前提下,模块也可以及时更新。

# HMR介绍

HMR是Webpack中最强大的功能之一,又名模块热替换(模块热更新)

扩展知识:热拔插

在一个正在运行的机器上随时插拔设备,不会对机器造成影响,设备插上立即可以使用。电脑的USB接口就可以热拔插

模块热替换(模块热更新)可以在应用运行过程中实时替换某个模块,应用的运行状态不受影响。我们使用热替换只将修改的模块实时替换至应用中,不必完全刷新应用,极大程度上提高了开发者的效率。

例如:我们在控制台写一些文字,在代码中修改css样式,浏览器中样式修改了但是文字没有重置。

# webpack开启HMR

HMR已经集成在webpack-dev-server中,不需要单独安装文件

开启方式:

  1. 使用的时候写webpack-dev-server --hot,后面添加--hot参数开启特性
  2. 在配置中开启
// 引入webpack模块,引入插件使用
const webpack = require('webpack')
module.exports = {
    ...
    devServer: {
        // 开启模块热更新
        hot: true
    }
    ...
    plugins: {
        new webpack.HotModuleReplacementPlugin()
    }
}    
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 修改项目中的css文件可以做到浏览器模块热更新,但是js文件不可以

# js模块热替换需要手动处理?!

What?!

webpack中的HMR斌不可以开箱即用,需要手动处理模块热替换逻辑。

# Q1:为什么样式文件的热更新开箱即用?

样式文件是经过Loader处理的,在style-loader中就自动处理了样式的热更新

image

# Q2:凭什么样式文件可以自动处理?

样式文件只需要将样式更新之后重新替换即可,很简单。

js文件是没有任何规律,很可能一段代码导出的是对象、字符串或者函数,对成员的使用也是多样的,所以webpack不知道如何实现一个通用的模块替换方案,所以为啥样式可以热更新下,js还是页面自动刷新的原因。

# Q3:我的项目没有手动处理,JS照样可以热替换

对vue-cli或者create-react脚手架工具的人来说,因为你使用的是框架,每种文件都是有规律的,如果都是以函数方式导出,那么直接修改某个函数就可以了。而且通过脚手架创建的项目内部都集成了HMR方案,所以不需要手动处理。

# 怎么手动处理js模块热替换?

HotModuleReplacementPlugin为我们的js提供了一套用于处理HMR的API

  1. 在webpack.config.js中配置
const webpack = require('webpack')

module.exports = (env, argv) => {
  // 开发模式下的选项配置
  const config = {
    ...
    plugins: [
      ...
      new webpack.HotModuleReplacementPlugin(),
      ...
    ]
  }

  return config
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. main.js是我们的入口文件,我们在这个文件中导入了其他模块,正因为这些模块中进行了更新,我们就应该重新使用这些模块。所以我们在要这个文件中处理他做依赖的这些模块更新过后的热替换。
// main.js
/**
 * 这个API中为我们的module提供了hot属性,这个hot是一个对象,是HMR的核心对象
 * hot对象下的accept方法用于注册某一个模块更新过后的处理函数
 * 
 *  第一个参数是依赖路径
 *  第二个参数就是依赖路径更新过后的处理函数
 */
module.hot.accept('./heading', () => {
  console.log('heading.js 模块更新了,这里需要手动处理模块')
})
1
2
3
4
5
6
7
8
9
10
11
  1. 修改heading.js中的一些代码,可以看到浏览器并没有自动刷新。

一旦这个模块被我们手动处理了,他就不会去触发自动刷新。

在回调函数中,我们根据自己的场景去看如何处理,因人而异这里给一个小例子看看即可:

// 创建一个元素追加到里面,那我们就再创建一个新元素追加进去。
let lastEditor = editor
module.hot.accept('./editor', () => {
    // 保留当前的文本值
    const value = lastEditor.innerHTML
    // 删除之前的元素
    document.body.removeChild(lastEditor)
    // 创建新的元素
    const newEditor = createEditor()
    newEditor.innerHTML = value
    document.body.appendChild(newEditor)
    // 新元素变成旧元素
    lastEditor = newEditor
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 图片模块热替换

在main.js中进行编辑

import icon from './icon.png'

const img = new Image()
img.src = icon
document.body.append(img)
// 图片热替换
module.hot.accept('./icon.png', () => {
  img.src = icon
})
1
2
3
4
5
6
7
8
9

这样编辑图片保存之后就会热替换。

# 注意事项

# Q1: 处理HMR的代码报错会导致自动刷新

使用hotOnly的配置,如果处理HMR的代码报错,也不采用自动刷新的形式

// 不需要启动自动更新作为热替换失败的后备
devServer: {
    hotOnly: true
}
1
2
3
4

# Q2:没启用HMR的情况下,HMR API报错

先去判断是否存在module.hot对象,再去处理业务热替换逻辑

if (module.hot) {
    
}
1
2
3

# Q3:我们是否写了很多与业务无关的代码

我们在webpack.config.js中关闭HMR的hot开关和插件配置,打包之后的bundle.js代码中没有相关代码,只有if (false) {}的无意义代码,在压缩之后也会去掉,所以根本不会影响生产环境中的运行状态。

更新时间: 2021-12-18 20:56