手记

Webpack 4 和单页应用入门(史上最全webpack4入门教程,看懂了你就入门了)

可以说是我目前看到最详细的 webpack 4 入门文章。
基本看完这个,基本也算一个合格的初级webpack配置工程师了。

webpack 更新到了 4.0,官网还没有更新文档。因此把教程更新一下,方便大家用起 webpack 4。

webpack

写在开头

先说说为什么要写这篇文章,最初的原因是组里的小朋友们看了 webpack 文档后,表情都是这样的:摘自 webpack 一篇文档的评论区)

wtf

是的,即使是外国佬也在吐槽这文档不是人能看的。回想起当年自己啃 webpack 文档的血与泪的往事,觉得有必要整一个教程,可以让大家看完后愉悦地搭建起一个 webpack 打包方案的项目。

官网新的 webpack 文档现在写的很详细了,能看英文的小伙伴可以直接去看官网。

可能会有人问 webpack 到底有什么用,你不能上来就糊我一脸代码让我马上搞,我照着搞了一遍结果根本没什么用,都是骗人的。所以,在说 webpack 之前,我想先谈一下前端打包方案这几年的演进历程,在什么场景下,我们遇到了什么问题,催生出了应对这些问题的工具。了解了需求和目的之后,你就知道什么时候 webpack 可以帮到你。我希望我用完之后很爽,你们用完之后也是。

先说说前端打包方案的黑暗历史

在很长的一段前端历史里,是不存在打包这个说法的。那个时候页面基本是纯静态的或者服务端输出的,没有 AJAX,也没有 jQuery。那个时候的 JavaScript 就像个玩具,用处大概就是在侧栏弄个时钟,用 media player 放个 mp3 之类的脚本,代码量不是很多,直接放在 <script> 标签里或者弄个 js 文件引一下就行,日子过得很轻松愉快。

随后的几年,人们开始尝试在一个页面里做更多的事情。容器的显示,隐藏,切换。用 css 写的弹层,图片轮播等等。但如果一个页面内不能向服务器请求数据,能做的事情毕竟有限的,代码的量也能维持在页面交互逻辑范围内。这时候很多人开始突破一个页面能做的事情的范围,使用隐藏的 iframe 和 flash 等作为和服务器通信的桥梁,新世界的大门慢慢地被打开,在一个页面内和服务器进行数据交互,意味着以前需要跳转多个页面的事情现在可以用一个页面搞定。但由于 iframe 和 flash 技术过于 tricky 和复杂,并没能得到广泛的推广。

直到 Google 推出 Gmail 的时候(2004 年),人们意识到了一个被忽略的接口,XMLHttpRequest, 也就是我们俗称的 AJAX, 这是一个使用方便的,兼容性良好的服务器通信接口。从此开始,我们的页面开始玩出各种花来了,前端一下子出现了各种各样的库,PrototypeDojoMooToolsExt JSjQuery…… 我们开始往页面里插入各种库和插件,我们的 js 文件也就爆炸了。

随着 js 能做的事情越来越多,引用越来越多,文件越来越大,加上当时大约只有 2Mbps 左右的网速,下载速度还不如 3G 网络,对 js 文件的压缩和合并的需求越来越强烈,当然这里面也有把代码混淆了不容易被盗用等其他因素在里面。JSMinYUI CompressorClosure CompilerUglifyJS 等 js 文件压缩合并工具陆陆续续诞生了。压缩工具是有了,但我们得要执行它,最简单的办法呢,就是 windows 上搞个 bat 脚本,mac / linux 上搞个 bash 脚本,哪几个文件要合并在一块的,哪几个要压缩的,发布的时候运行一下脚本,生成压缩后的文件。

基于合并压缩技术,项目越做越大,问题也越来越多,大概就是以下这些问题:

  • 库和插件为了要给他人调用,肯定要找个地方注册,一般就是在 window 下申明一个全局的函数或对象。难保哪天用的两个库在全局用同样的名字,那就冲突了。

  • 库和插件如果还依赖其他的库和插件,就要告知使用人,需要先引哪些依赖库,那些依赖库也有自己的依赖库的话,就要先引依赖库的依赖库,以此类推。

恰好就在这个时候(2009 年),随着后端 JavaScript 技术的发展,人们提出了 CommonJS 的模块化规范,大概的语法是: 如果 a.js 依赖 b.jsc.js, 那么就在 a.js 的头部,引入这些依赖文件:

var b = require('./b')var c = require('./c')

那么变量 bc 会是什么呢?那就是 b.js 和 c.js 导出的东西,比如 b.js 可以这样导出:

exports.square = function(num) {  return num * num
}

然后就可以在 a.js 使用这个 square 方法:

var n = b.square(2)

如果 c.js 依赖 d.js, 导出的是一个 Number, 那么可以这样写:

var d = require('./d')module.exports = d.PI // 假设 d.PI 的值是 3.14159

那么 a.js 中的变量 c 就是数字 3.14159,具体的语法规范可以查看 Node.js 的 文档

但是 CommonJS 在浏览器内并不适用。因为 require() 的返回是同步的,意味着有多个依赖的话需要一个一个依次下载,堵塞了 js 脚本的执行。所以人们就在 CommonJS 的基础上定义了 Asynchronous Module Definition (AMD) 规范(2011 年),使用了异步回调的语法来并行下载多个依赖项,比如作为入口的 a.js 可以这样写:

require(['./b', './c'], function(b, c) {  var n = b.square(2)  console.log(c)
})

相应的导出语法也是异步回调方式,比如 c.js 依赖 d.js, 就写成这样:

define(['./d'], function(d) {  return d.PI
})

可以看到,定义一个模块是使用 define() 函数,define()require() 的区别是,define() 必须要在回调函数中返回一个值作为导出的东西,require() 不需要导出东西,因此回调函数中不需要返回值,也无法作为被依赖项被其他文件导入,因此一般用于入口文件,比如页面中这样加载 a.js:

<script src="js/require.js" data-main="js/a"></script>

以上是 AMD 规范的基本用法,更详细的就不多说了(反正也淘汰了~),有兴趣的可以看 这里

js 模块化问题基本解决了,css 和 html 也没闲着。什么 lesssassstylus 的 css 预处理器横空出世,说能帮我们简化 css 的写法,自动给你加 vendor prefix。html 在这期间也出现了一堆模板语言,什么 handlebarsejsjade,可以把 ajax 拿到的数据插入到模板中,然后用 innerHTML 显示到页面上。

托 AMD 和 CSS 预处理和模板语言的福,我们的编译脚本也洋洋洒洒写了百来行。命令行脚本有个不好的地方,就是 windows 和 mac/linux 是不通用的,如果有跨平台需求的话,windows 要装个可以执行 bash 脚本的命令行工具,比如 msys(目前最新的是 msys2),或者使用 php 或 python 等其他语言的脚本来编写,对于非全栈型的前端程序员来说,写 bash / php / python 还是很生涩的。因此我们需要一个简单的打包工具,可以利用各种编译工具,编译 / 压缩 js、css、html、图片等资源。然后 Grunt 产生了(2012 年),配置文件格式是我们最爱的 js,写法也很简单,社区有非常多的插件支持各种编译、lint、测试工具。一年多后另一个打包工具 gulp 诞生了,扩展性更强,采用流式处理效率更高。

依托 AMD 模块化编程,SPA(Single-page application) 的实现方式更为简单清晰,一个网页不再是传统的类似 word 文档的页面,而是一个完整的应用程序。SPA 应用有一个总的入口页面,我们通常把它命名为 index.html、app.html、main.html,这个 html 的 <body> 一般是空的,或者只有总的布局(layout),比如下图:

image

布局会把 header、nav、footer 的内容填上,但 main 区域是个空的容器。这个作为入口的 html 最主要的工作是加载启动 SPA 的 js 文件,然后由 js 驱动,根据当前浏览器地址进行路由分发,加载对应的 AMD 模块,然后该 AMD 模块执行,渲染对应的 html 到页面指定的容器内(比如图中的 main)。在点击链接等交互时,页面不会跳转,而是由 js 路由加载对应的 AMD 模块,然后该 AMD 模块渲染对应的 html 到容器内。

虽然 AMD 模块让 SPA 更容易地实现,但小问题还是很多的:

  • 不是所有的第三方库都是 AMD 规范的,这时候要配置 shim,很麻烦。

  • 虽然 RequireJS 支持通过插件把 html 作为依赖加载,但 html 里面的 <img> 的路径是个问题,需要使用绝对路径并且保持打包后的图片路径和打包前的路径不变,或者使用 html 模板语言把 src 写成变量,在运行时生成。

  • 不支持动态加载 css,变通的方法是把所有的 css 文件合并压缩成一个文件,在入口的 html 页面一次性加载。

  • SPA 项目越做越大,一个应用打包后的 js 文件到了几 MB 的大小。虽然 r.js 支持分模块打包,但配置很麻烦,因为模块之间会互相依赖,在配置的时候需要 exclude 那些通用的依赖项,而依赖项要在文件里一个个检查。

  • 所有的第三方库都要自己一个个的下载,解压,放到某个目录下,更别提更新有多麻烦了。虽然可以用 npm 包管理工具,但 npm 的包都是 CommonJS 规范的,给后端 Node.js 用的,只有部分支持 AMD 规范,而且在 npm 3 之前,这些包有依赖项的话也是不能用的。后来有个 bower 包管理工具是专门的 web 前端仓库,这里的包一般都支持 AMD 规范。

  • AMD 规范定义和引用模块的语法太麻烦,上面介绍的 AMD 语法仅是最简单通用的语法,API 文档里面还有很多变异的写法,特别是当发生循环引用的时候(a 依赖 b,b 依赖 a),需要使用其他的 语法 解决这个问题。而且 npm 上很多前后端通用的库都是 CommonJS 的语法。后来很多人又开始尝试使用 ES6 模块规范,如何引用 ES6 模块又是一个大问题。

  • 项目的文件结构不合理,因为 grunt/gulp 是按照文件格式批量处理的,所以一般会把 js、html、css、图片分别放在不同的目录下,所以同一个模块的文件会散落在不同的目录下,开发的时候找文件是个麻烦的事情。code review 时想知道一个文件是哪个模块的也很麻烦,解决办法比如又要在 imgs 目录下建立按模块命名的文件夹,里面再放图片。

到了这里,我们的主角 webpack 登场了(2012 年)(此处应有掌声)。

和 webpack 差不多同期登场的还有 Browserify。这里简单介绍一下 Browserify。Browserify 的目的是让前端也能用 CommonJS 的语法 require('module') 来加载 js。它会从入口 js 文件开始,把所有的 require() 调用的文件打包合并到一个文件,这样就解决了异步加载的问题。那么 Browserify 有什么不足之处导致我不推荐使用它呢? 主要原因有下面几点:

  • 最主要的一点,Browserify 不支持把代码打包成多个文件,在有需要的时候加载。这就意味着访问任何一个页面都会全量加载所有文件。

  • Browserify 对其他非 js 文件的加载不够完善,因为它主要解决的是 require() js 模块的问题,其他文件不是它关心的部分。比如 html 文件里的 img 标签,它只能转成 Data URI 的形式,而不能替换为打包后的路径。

  • 因为上面一点 Browserify 对资源文件的加载支持不够完善,导致打包时一般都要配合 gulp 或 grunt 一块使用,无谓地增加了打包的难度。

  • Browserify 只支持 CommonJS 模块规范,不支持 AMD 和 ES6 模块规范,这意味旧的 AMD 模块和将来的 ES6 模块不能使用。

基于以上几点,Browserify 并不是一个理想的选择。那么 webpack 是否解决了以上的几个问题呢? 废话,不然介绍它干嘛。那么下面章节我们用实战的方式来说明 webpack 是怎么解决上述的问题的。

上手先搞一个简单的 SPA 应用

一上来步子太大容易扯到蛋,让我们先弄个最简单的 webpack 配置来热一下身。

安装 Node.js

webpack 是基于我大 Node.js 的打包工具,上来第一件事自然是先安装 Node.js 了,传送门 ->

初始化一个项目

我们先随便找个地方,建一个文件夹叫 simple, 然后在这里面搭项目。完成品在 examples/simple 目录,大家搞的时候可以参照一下。我们先看一下目录结构:

├── dist                      打包输出目录,只需部署这个目录到生产环境
├── package.json              项目配置信息
├── node_modules              npm 安装的依赖包都在这里面
├── src                       我们的源代码
│   ├── components            可以复用的模块放在这里面
│   ├── index.html            入口 html│   ├── index.js              入口 js│   ├── shared                公共函数库
│   └── views                 页面放这里
└── webpack.config.js         webpack 配置文件

打开命令行窗口,cd 到刚才建的 simple 目录。然后执行这个命令初始化项目:

npm init

命令行会要你输入一些配置信息,我们这里一路按回车下去,生成一个默认的项目配置文件 package.json

给项目加上语法报错和代码规范检查

我们安装 eslint, 用来检查语法报错,当我们书写 js 时,有错误的地方会出现提示。

npm install eslint eslint-config-enough babel-eslint eslint-loader --save-dev

npm install 可以一条命令同时安装多个包,包之间用空格分隔。包会被安装进 node_modules 目录中。

--save-dev 会把安装的包和版本号记录到 package.json 中的 devDependencies 对象中,还有一个 --save, 会记录到 dependencies 对象中,它们的区别,我们可以先简单的理解为打包工具和测试工具用到的包使用 --save-dev 存到 devDependencies, 比如 eslint、webpack。浏览器中执行的 js 用到的包存到 dependencies, 比如 jQuery 等。那么它们用来干嘛的?

因为有些 npm 包安装是需要编译的,那么导致 windows / mac /linux 上编译出的可执行文件是不同的,也就是无法通用,因此我们在提交代码到 git 上去的时候,一般都会在 .gitignore 里指定忽略 node_modules 目录和里面的文件,这样其他人从 git 上拉下来的项目是没有 node_modules 目录的,这时我们需要运行

npm install

它会读取 package.json 中的 devDependenciesdependencies 字段,把记录的包的相应版本下载下来。

这里 eslint-config-enough 是配置文件,它规定了代码规范,要使它生效,我们要在 package.json 中添加内容:

{  "eslintConfig": {    "extends": "enough",    "env": {      "browser": true,      "node": true
    }
  }
}

业界最有名的语法规范是 airbnb 出品的,但它规定的太死板了,比如不允许使用 for-offor-in 等。感兴趣的同学可以参照 这里 安装使用。

babel-eslinteslint-config-enough 依赖的语法解析库,替代 eslint 默认的解析库以支持还未标准化的语法。比如 import()

eslint-loader 用于在 webpack 编译的时候检查代码,如果有错误,webpack 会报错。

项目里安装了 eslint 还没用,我们的 IDE 和编辑器也得要装 eslint 插件支持它。

Visual Studio Code 需要安装 ESLint 扩展

atom 需要安装 linterlinter-eslint 这两个插件,装好后重启生效。

WebStorm 需要在设置中打开 eslint 开关:

image

写几个页面

我们写一个最简单的 SPA 应用来介绍 SPA 应用的内部工作原理。首先,建立 src/index.html 文件,内容如下:

<!DOCTYPE html><html>
  <head>
    <meta charset="utf-8">
  </head>

  <body>
  </body></html>

它是一个空白页面,注意这里我们不需要自己写 <script src="index.js"></script>, 因为打包后的文件名和路径可能会变,所以我们用 webpack 插件帮我们自动加上。

src/index.js:

// 引入 routerimport router from './router'// 启动 routerrouter.start()

src/router.js:

// 引入页面文件import foo from './views/foo'import bar from './views/bar'const routes = {  '/foo': foo,  '/bar': bar
}// Router 类,用来控制页面根据当前 URL 切换class Router {
  start() {    // 点击浏览器后退 / 前进按钮时会触发 window.onpopstate 事件,我们在这时切换到相应页面
    // https://developer.mozilla.org/en-US/docs/Web/Events/popstate
    window.addEventListener('popstate', () => {      this.load(location.pathname)
    })    // 打开页面时加载当前页面
    this.load(location.pathname)
  }  // 前往 path,变更地址栏 URL,并加载相应页面
  go(path) {    // 变更地址栏 URL
    history.pushState({}, '', path)    // 加载页面
    this.load(path)
  }  // 加载 path 路径的页面
  load(path) {    // 首页
    if (path === '/') path = '/foo'
    // 创建页面实例
    const view = new routes[path]()    // 调用页面方法,把页面加载到 document.body 中
    view.mount(document.body)
  }
}// 导出 router 实例export default new Router()

src/views/foo/index.js:

// 引入 routerimport router from '../../router'// 引入 html 模板,会被作为字符串引入import template from './index.html'// 引入 css, 会生成 <style> 块插入到 <head> 头中import './style.css'// 导出类export default class {
  mount(container) {    document.title = 'foo'
    container.innerHTML = template
    container.querySelector('.foo__gobar').addEventListener('click', () => {      // 调用 router.go 方法加载 /bar 页面
      router.go('/bar')
    })
  }
}

src/views/bar/index.js:

// 引入 routerimport router from '../../router'// 引入 html 模板,会被作为字符串引入import template from './index.html'// 引入 css, 会生成 <style> 块插入到 <head> 头中import './style.css'// 导出类export default class {
  mount(container) {    document.title = 'bar'
    container.innerHTML = template
    container.querySelector('.bar__gofoo').addEventListener('click', () => {      // 调用 router.go 方法加载 /foo 页面
      router.go('/foo')
    })
  }
}

借助 webpack 插件,我们可以 import html, css 等其他格式的文件,文本类的文件会被储存为变量打包进 js 文件,其他二进制类的文件,比如图片,可以自己配置,小图片作为 Data URI 打包进 js 文件,大文件打包为单独文件,我们稍后再讲这块。

其他的 src 目录下的文件大家自己浏览,拷贝一份到自己的工作目录,等会打包时会用到。

页面代码这样就差不多搞定了,接下来我们进入 webpack 的安装和配置阶段。现在我们还没有讲 webpack 配置所以页面还无法访问,等会弄好 webpack 配置后再看页面实际效果。

安装 webpack 和 Babel

我们把 webpack 和它的插件安装到项目:

npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev

webpack 即 webpack 核心库。它提供了很多 API, 通过 Node.js 脚本中 require('webpack') 的方式来使用 webpack。

webpack-cli 是 webpack 的命令行工具。让我们可以不用写打包脚本,只需配置打包配置文件,然后在命令行输入 webpack-cli --config webpack.config.js 来使用 webpack, 简单很多。webpack 4 之前命令行工具是集成在 webpack 包中的,4.0 开始 webpack 包本身不再集成 cli。

webpack-serve 是 webpack 提供的用来开发调试的服务器,让你可以用 http://127.0.0.1:8080/ 这样的 url 打开页面来调试,有了它就不用配置 nginx 了,方便很多。

html-webpack-plugin, html-loader, css-loader, style-loader 等看名字就知道是打包 html 文件,css 文件的插件,大家在这里可能会有疑问,html-webpack-pluginhtml-loader 有什么区别,css-loaderstyle-loader 有什么区别,我们等会看配置文件的时候再讲。

file-loaderurl-loader 是打包二进制文件的插件,具体也在配置文件章节讲解。

接下来,为了能让不支持 ES6 的浏览器 (比如 IE) 也能照常运行,我们需要安装 babel, 它会把我们写的 ES6 源代码转化成 ES5,这样我们源代码写 ES6,打包时生成 ES5。

npm install babel-core babel-preset-env babel-loader --save-dev

这里 babel-core 顾名思义是 babel 的核心编译器。babel-preset-env 是一个配置文件,我们可以使用这个配置文件转换 ES2015/ES2016/ES2017 到 ES5,是的,不只 ES6 哦。babel 还有 其他配置文件

光安装了 babel-preset-env,在打包时是不会生效的,需要在 package.json 加入 babel 配置:

{  "babel": {    "presets": ["env"]
  }
}

打包时 babel 会读取 package.jsonbabel 字段的内容,然后执行相应的转换。

babel-loader 是 webpack 的插件,我们下面章节再说。

配置 webpack

包都装好了,接下来总算可以进入正题了。我们来创建 webpack 配置文件 webpack.config.js,注意这个文件是在 node.js 中运行的,因此不支持 ES6 的 import 语法。我们来看文件内容:

const { resolve } = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin')const history = require('connect-history-api-fallback')const convert = require('koa-connect')// 使用 WEBPACK_SERVE 环境变量检测当前是否是在 webpack-server 启动的开发环境中const dev = Boolean(process.env.WEBPACK_SERVE)module.exports = {  /*
  webpack 执行模式
  development:开发环境,它会在配置文件中插入调试相关的选项,比如 moduleId 使用文件路径方便调试
  production:生产环境,webpack 会将代码做压缩等优化
  */
  mode: dev ? 'development' : 'production',  /*
  配置 source map
  开发模式下使用 cheap-module-eval-source-map, 生成的 source map 能和源码每行对应,方便打断点调试
  生产模式下使用 hidden-source-map, 生成独立的 source map 文件,并且不在 js 文件中插入 source map 路径,用于在 error report 工具中查看 (比如 Sentry)
  */
  devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map',  // 配置页面入口 js 文件
  entry: './src/index.js',  // 配置打包输出相关
  output: {    // 打包输出目录
    path: resolve(__dirname, 'dist'),    // 入口 js 的打包输出文件名
    filename: 'index.js'
  },  module: {    /*
    配置各种类型文件的加载器,称之为 loader
    webpack 当遇到 import ... 时,会调用这里配置的 loader 对引用的文件进行编译
    */
    rules: [
      {        /*
        使用 babel 编译 ES6 / ES7 / ES8 为 ES5 代码
        使用正则表达式匹配后缀名为 .js 的文件
        */
        test: /\.js$/,        // 排除 node_modules 目录下的文件,npm 安装的包不需要编译
        exclude: /node_modules/,        /*
        use 指定该文件的 loader, 值可以是字符串或者数组。
        这里先使用 eslint-loader 处理,返回的结果交给 babel-loader 处理。loader 的处理顺序是从最后一个到第一个。
        eslint-loader 用来检查代码,如果有错误,编译的时候会报错。
        babel-loader 用来编译 js 文件。
        */
        use: ['babel-loader', 'eslint-loader']
      },

      {        // 匹配 html 文件
        test: /\.html$/,        /*
        使用 html-loader, 将 html 内容存为 js 字符串,比如当遇到
        import htmlString from './template.html';
        template.html 的文件内容会被转成一个 js 字符串,合并到 js 文件里。
        */
        use: 'html-loader'
      },

      {        // 匹配 css 文件
        test: /\.css$/,        /*
        先使用 css-loader 处理,返回的结果交给 style-loader 处理。
        css-loader 将 css 内容存为 js 字符串,并且会把 background, @font-face 等引用的图片,
        字体文件交给指定的 loader 打包,类似上面的 html-loader, 用什么 loader 同样在 loaders 对象中定义,等会下面就会看到。
        */
        use: ['style-loader', 'css-loader']
      },

      {        /*
        匹配各种格式的图片和字体文件
        上面 html-loader 会把 html 中 <img> 标签的图片解析出来,文件名匹配到这里的 test 的正则表达式,
        css-loader 引用的图片和字体同样会匹配到这里的 test 条件
        */
        test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,        /*
        使用 url-loader, 它接受一个 limit 参数,单位为字节(byte)

        当文件体积小于 limit 时,url-loader 把文件转为 Data URI 的格式内联到引用的地方
        当文件大于 limit 时,url-loader 会调用 file-loader, 把文件储存到输出目录,并把引用的文件路径改写成输出后的路径

        比如 views/foo/index.html 中
        <img src="smallpic.png">
        会被编译成
        <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAA...">

        而
        <img src="largepic.png">
        会被编译成
        <img src="/f78661bef717cf2cc2c2e5158f196384.png">
        */
        use: [
          {            loader: 'url-loader',            options: {              limit: 10000
            }
          }
        ]
      }
    ]
  },  /*
  配置 webpack 插件
  plugin 和 loader 的区别是,loader 是在 import 时根据不同的文件名,匹配不同的 loader 对这个文件做处理,
  而 plugin, 关注的不是文件的格式,而是在编译的各个阶段,会触发不同的事件,让你可以干预每个编译阶段。
  */
  plugins: [    /*
    html-webpack-plugin 用来打包入口 html 文件
    entry 配置的入口是 js 文件,webpack 以 js 文件为入口,遇到 import, 用配置的 loader 加载引入文件
    但作为浏览器打开的入口 html, 是引用入口 js 的文件,它在整个编译过程的外面,
    所以,我们需要 html-webpack-plugin 来打包作为入口的 html 文件
    */
    new HtmlWebpackPlugin({      /*
      template 参数指定入口 html 文件路径,插件会把这个文件交给 webpack 去编译,
      webpack 按照正常流程,找到 loaders 中 test 条件匹配的 loader 来编译,那么这里 html-loader 就是匹配的 loader
      html-loader 编译后产生的字符串,会由 html-webpack-plugin 储存为 html 文件到输出目录,默认文件名为 index.html
      可以通过 filename 参数指定输出的文件名
      html-webpack-plugin 也可以不指定 template 参数,它会使用默认的 html 模板。
      */
      template: './src/index.html',      /*
      因为和 webpack 4 的兼容性问题,chunksSortMode 参数需要设置为 none
      https://github.com/jantimon/html-webpack-plugin/issues/870
      */
      chunksSortMode: 'none'
    })
  ]
}/*
配置开发时用的服务器,让你可以用 http://127.0.0.1:8080/ 这样的 url 打开页面来调试
并且带有热更新的功能,打代码时保存一下文件,浏览器会自动刷新。比 nginx 方便很多
如果是修改 css, 甚至不需要刷新页面,直接生效。这让像弹框这种需要点击交互后才会出来的东西调试起来方便很多。

因为 webpack-cli 无法正确识别 serve 选项,使用 webpack-cli 执行打包时会报错。
因此我们在这里判断一下,仅当使用 webpack-serve 时插入 serve 选项。
issue:https://github.com/webpack-contrib/webpack-serve/issues/19
*/if (dev) {  module.exports.serve = {    // 配置监听端口,默认值 8080
    port: 8080,    // add: 用来给服务器的 koa 实例注入 middleware 增加功能
    add: app => {      /*
      配置 SPA 入口

      SPA 的入口是一个统一的 html 文件,比如
      http://localhost:8080/foo
      我们要返回给它
      http://localhost:8080/index.html
      这个文件
      */
      app.use(convert(history()))
    }
  }
}



作者:冷落i
链接:https://www.jianshu.com/p/991e1067eee0


1人推荐
随时随地看视频
慕课网APP