Webpack 4 和单页运用入门

Github:
https://github.com/fenivana/w…

webpack 更新到了 4.0,官网还没有更新文档。因而把教程更新一下,轻易人人用起 webpack 4。

《Webpack 4 和单页运用入门》

写在开首

先说说为何要写这篇文章,最初的原因是组里的小朋友们看了 webpack 文档后,脸色都是如许的:摘自 webpack 一篇文档的批评区)

《Webpack 4 和单页运用入门》

和如许的:

《Webpack 4 和单页运用入门》

是的,即使是外国佬也在吐槽这文档不是人能看的。回想起昔时自身啃 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),比方下图:

《Webpack 4 和单页运用入门》

规划会把 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 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 等。感兴致的同砚可以参照 这里 装置运用。

eslint-loader 用于在 webpack 编译的时刻搜检代码,假如有毛病,webpack 会报错。

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

Visual Studio Code 须要装置 ESLint 扩大

atom 须要装置 linterlinter-eslint 这两个插件,装好后重启见效。

WebStorm 须要在设置中翻开 eslint 开关:

《Webpack 4 和单页运用入门》

写几个页面

我们写一个最简朴的 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:

// 引入 router
import router from './router'

// 启动 router
router.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:

// 引入 router
import 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:

// 引入 router
import 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()))
    }
  }
}

走一个

设置 OK 了,接下来我们就运转一下吧。我们先试一下开辟环境用的 webpack-serve:

./node_modules/.bin/webpack-serve webpack.config.js

实行时须要指定设置文件。

上面的敕令实用于 Mac / Linux 等 * nix 体系,也实用于 Windows 上的 PowerShell 和 bash/zsh 环境(Windows Subsystem for Linux, Git BashBabunMSYS2 等)。安利一下 Windows 同砚运用 Ubuntu on Windows,可以防备许多跨平台的题目,比方设置环境变量。

假如运用 Windows 的 cmd.exe,请实行:

node_modules\.bin\webpack-serve webpack.config.js

npm 会把包的可实行文件装置到 ./node_modules/.bin/ 目次下,所以我们要在这个目次下实行敕令。

敕令实行后,掌握台显现:

「wdm」: Compiled successfully。

这就代表编译胜利了,我们可以在阅读器翻开 http://localhost:8080/ 看看结果。假如有报错,那多是什么处所没弄对?请自身细致搜检一下~

我们可以随便变动一下 src 目次下的源代码,保存后,阅读器里的页面应当很快会有响应变化。

要退出编译,按 ctrl+c

开辟环境编译试过以后,我们碰运气编译临盆环境的代码,敕令是:

./node_modules/.bin/webpack-cli

不须要指定设置文件,默许读取 webpack.config.js

实行剧本的敕令有点贫苦,因而,我们可以运用 npm,把敕令写在 package.json 中:

{
  "scripts": {
    "dev": "webpack-serve webpack.config.js",
    "build": "webpack-cli"
  }
}

package.json 中的 scripts 对象,可以用来写一些剧本敕令,敕令不须要前缀目次 ./node_modules/.bin/,npm 会自动寻觅该目次下的敕令。我们可以实行:

npm run dev

来启动开辟环境。

实行

npm run build

来打包临盆环境的代码。

进阶设置

上面的项目虽然可以跑起来了,但有几个点我们还没有考虑到:

  • 设置静态资本的 url 途径前缀
  • 各个页面离开打包
  • 第三方库和营业代码离开打包
  • 输出的 entry 文件加上 hash
  • 开辟环境封闭 performance.hints
  • 设置 favicon
  • 开辟环境许可其他电脑接见
  • 打包时自定义部份参数
  • webpack-serve 处置惩罚途径带后缀名的文件的特别划定规矩
  • 代码中插进去环境变量
  • 简化 import 途径
  • 优化 babel 编译后的代码机能
  • 运用 webpack 自带的 ES6 模块处置惩罚功用
  • 运用 autoprefixer 自动建立 css 的 vendor prefixes

那末,让我们在上面的设置的基本上继承完美,下面的代码我们只写出转变的部份。代码在 examples/advanced 目次。

设置静态资本的 url 途径前缀

如今我们的资本文件的 url 直接在根目次,比方 http://127.0.0.1:8080/index.js, 如许做缓存掌握和 CDN 不是很轻易,因而我们给资本文件的 url 加一个前缀,比方 http://127.0.0.1:8080/assets/index.js. 我们来修正一下 webpack 设置:

{
  output: {
    publicPath: '/assets/'
  }
}

webpack-serve 也须要修正:

if (dev) {
  module.exports.serve = {
    port: 8080,
    host: '0.0.0.0',
    dev: {
      /*
      指定 webpack-dev-middleware 的 publicpath
      平常状况下与 output.publicPath 坚持一致(除非 output.publicPath 运用的是相对途径)
      https://github.com/webpack/webpack-dev-middleware#publicpath
      */
      publicPath: '/assets/'
    },
    add: app => {
      app.use(convert(history({
        index: '/assets/' // index.html 文件在 /assets/ 途径下
      })))
    }
  }
}

各个页面离开打包

如许阅读器只需加载当前页面所需的代码。

webpack 可以运用异步加载文件的体式格局援用模块,我们运用 async/
awaitdynamic import 来完成:

src/router.js:

// 将 async/await 转换成 ES5 代码后须要这个运转时库来支撑
import 'regenerator-runtime/runtime'

const routes = {
  // import() 返回 promise
  '/foo': () => import('./views/foo'),
  '/bar.do': () => import('./views/bar.do')
}

class Router {
  // ...

  // 加载 path 途径的页面
  // 运用 async/await 语法
  async load(path) {
    // 首页
    if (path === '/') path = '/foo'

    // 动态加载页面
    const View = (await routes[path]()).default

    // 建立页面实例
    const view = new View()

    // 挪用页面要领,把页面加载到 document.body 中
    view.mount(document.body)
  }
}

如许我们就不须要在开首把一切页面文件都 import 进来了。

由于 import() 还没有正式进入规范,须要运用 babel-preset-stage-2 来支撑:

npm install babel-preset-stage-2 --save-dev

package.json 改一下:

{
  "babel": {
    "presets": [
      "env",
      "stage-2"
    ]
  }
}

然后修正 webpack 设置:

{
  output: {
    /*
    代码中援用的文件(js、css、图片等)会依据设置兼并为一个或多个包,我们称一个包为 chunk。
    每一个 chunk 包括多个 modules。不管是不是是 js,webpack 都将引入的文件视为一个 module。
    chunkFilename 用来设置这个 chunk 输出的文件名。

    [chunkhash]:这个 chunk 的 hash 值,文件发作变化时该值也会变。运用 [chunkhash] 作为文件名可以防备阅读器读取旧的缓存文件。

    另有一个占位符 [id],编译时每一个 chunk 会有一个id。
    我们在这里不运用它,由于这个 id 是个递增的数字,增添或削减一个chunk,都可以致使其他 chunk 的 id 发作转变,致使缓存失效。
    */
    chunkFilename: '[chunkhash].js',
  }
}

第三方库和营业代码离开打包

如许更新营业代码时可以借助阅读器缓存,用户不须要从新下载没有发作变化的第三方库。
Webpack 4 最大的革新就是自动拆分 chunk, 假如同时满足以下前提,chunk 就会被拆分:

  • 新的 chunk 能被复用,或许模块是来自 node_modules 目次
  • 新的 chunk 大于 30Kb(min+gz 紧缩前)
  • 按需加载 chunk 的并发要求数目小于即是 5 个
  • 页面初始加载时的并发要求数目小于即是 3 个

平常状况只需设置这几个参数即可:

{
  plugins: [
    // ...

    /*
    运用文件途径的 hash 作为 moduleId。
    虽然我们运用 [chunkhash] 作为 chunk 的输着名,但仍然不够。
    由于 chunk 内部的每一个 module 都有一个 id,webpack 默许运用递增的数字作为 moduleId。
    假如引入了一个新文件或删掉一个文件,可以会致使其他文件的 moduleId 也发作转变,
    那末受影响的 module 地点的 chunk 的 [chunkhash] 就会发作转变,致使缓存失效。
    因而运用文件途径的 hash 作为 moduleId 来防备这个题目。
    */
    new webpack.HashedModuleIdsPlugin()
  ],

  optimization: {
    /*
    上面提到 chunkFilename 指定了 chunk 打包输出的名字,那末文件名存在那里了呢?
    它就存在援用它的文件中。这意味着一个 chunk 文件名发作转变,会致使援用这个 chunk 文件也发作转变。

    runtimeChunk 设置为 true, webpack 就会把 chunk 文件名悉数存到一个零丁的 chunk 中,
    如许更新一个文件只会影响到它地点的 chunk 和 runtimeChunk,防备了援用这个 chunk 的文件也发作转变。
    */
    runtimeChunk: true,

    splitChunks: {
      /*
      默许 entry 的 chunk 不会被拆分
      由于我们运用了 html-webpack-plugin 来动态插进去 <script> 标签,entry 被拆成多个 chunk 也能自动被插进去到 html 中,
      所以我们可以设置成 all, 把 entry chunk 也拆分了
      */
      chunks: 'all'
    }
  }
}

webpack 4 支撑更多的手动优化,详见: https://gist.github.com/sokra…

但正如 webpack 文档中所说,默许设置已充足优化,在没有测试的状况下不要自觉手动优化。

输出的 entry 文件加上 hash

上面我们提到了 chunkFilename 运用 [chunkhash] 防备阅读器读取毛病缓存,那末 entry 一样须要加上 hash。
但运用 webpack-serve 启动开辟环境时,entry 文件是没有 [chunkhash] 的,用了会报错。
因而我们只在实行 webpack-cli 时运用 [chunkhash]

{
  output: {
    filename: dev ? '[name].js' : '[chunkhash].js'
  }
}

这里我们运用了 [name] 占位符。诠释它之前我们先相识一下 entry 的完整定义:

{
  entry: {
    NAME: [FILE1, FILE2, ...]
  }
}

我们可以定义多个 entry 文件,比方你的项目有多个 html 进口文件,每一个 html 对应一个或多个 entry 文件。
然后每一个 entry 可以定义由多个 module 构成,这些 module 会顺次实行。
在 webpack 4 之前,这是很有效的功用,比方之前提到的第三方库和营业代码离开打包,在之前,我们须要这么设置:

{
  entry {
    main: './src/index.js',
    vendor: ['jquery', 'lodash']
  }
}

entry 援用文件的划定规矩和 import 是一样的,会寻觅 node_modules 里的包。然后连系 CommonsChunkPlugin 把 vendor 定义的 module 从营业代码分离出来打包成一个零丁的 chunk。
假如 entry 是一个 module,我们可以不运用数组的情势。

在 simple 项目中,我们设置了 entry: './src/index.js',这是最简朴的情势,转换成完整的写法就是:

{
  entry: {
    main: ['./src/index.js']
  }
}

webpack 会给这个 entry 指定名字为 main

看到这应当晓得 [name] 的意义了吧?它就是 entry 的名字。

有人可以注重到官网文档中另有一个 [hash] 占位符,这个 hash 是悉数编译历程发生的一个总的 hash 值,而不是单个文件的 hash 值,项目中任何一个文件的修改,都邑形成这个 hash 值的转变。[hash] 占位符是一直存在的,但我们不愿望修正一个文件致使一切输出的文件 hash 都转变,如许就没法运用阅读器缓存了。因而这个 [hash] 意义不大。

开辟环境封闭 performance.hints

我们注重到运转开辟环境是敕令行会报一段 warning:

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (250 kB).
This can impact web performance.

这是说发起每一个输出的 js 文件的大小不要凌驾 250k。但开辟环境由于包括了 sourcemap 而且代码未紧缩所以平常都邑凌驾这个大小,所以我们可以在开辟环境把这个 warning 封闭。

webpack 设置中到场:

{
  performance: {
    hints: dev ? false : 'warning'
  }
}

设置 favicon

在 src 目次中放一张 favicon.png,然后 src/index.html<head> 中插进去:

<link rel="icon" type="image/png" href="favicon.png">

修正 webpack 设置:

{
  module: {
    rules: [
      {
        test: /\.html$/,
        use: [
          {
            loader: 'html-loader',
            options: {
              /*
              html-loader 接收 attrs 参数,示意什么标签的什么属性须要挪用 webpack 的 loader 举行打包。
              比方 <img> 标签的 src 属性,webpack 会把 <img> 援用的图片打包,然后 src 的属性值替代为打包后的途径。
              运用什么 loader 代码,一样是在 module.rules 定义中运用婚配的划定规矩。

              假如 html-loader 不指定 attrs 参数,默许值是 img:src, 意味着会默许打包 <img> 标签的图片。
              这里我们加上 <link> 标签的 href 属性,用来打包进口 index.html 引入的 favicon.png 文件。
              */
              attrs: ['img:src', 'link:href']
            }
          }
        ]
      },

      {
        /*
        婚配 favicon.png
        上面的 html-loader 会把进口 index.html 援用的 favicon.png 图标文件剖析出来举行打包
        打包划定规矩就根据这里指定的 loader 实行
        */
        test: /favicon\.png$/,

        use: [
          {
            // 运用 file-loader
            loader: 'file-loader',
            options: {
              /*
              name:指定文件输着名
              [hash] 为源文件的hash值,[ext] 为后缀。
              */
              name: '[hash].[ext]'
            }
          }
        ]
      },

      // 图片文件的加载设置增添一个 exclude 参数
      {
        test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,

        // 消除 favicon.png, 由于它已由上面的 loader 处置惩罚了。假如不消撤除,它会被这个 loader 再处置惩罚一遍
        exclude: /favicon\.png$/,

        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10000
            }
          }
        ]
      }
    ]
  }
}

实在 html-webpack-plugin 接收一个 favicon 参数,可以指定 favicon 文件途径,会自动打包插进去到 html 文件中。但它有个 bug,打包后的文件名途径不带 hash,就算有 hash,它也是 [hash],而不是 [chunkhash]。致使修正代码也会转变 favicon 打包输出的文件名。issue 中提到的 favicons-webpack-plugin 却是可以用,但它依靠 PhantomJS, 异常大。

开辟环境许可其他电脑接见

const internalIp = require('internal-ip')

module.exports.serve = {
  host: '0.0.0.0',
  hot: {
    host: {
      client: internalIp.v4.sync(),
      server: '0.0.0.0'
    }
  },
  
  // ...
}

打包时自定义部份参数

在多人开辟时,每一个人可以须要有自身的设置,比方说 webpack-serve 监听的端口号,假如写死在 webpack 设置里,而谁人端口号在某个同砚的电脑上被其他历程占用了,简朴粗犷的修正 webpack.config.js 会致使提交代码后其他同砚的端口也被改掉。

另有一点就是开辟环境、测试环境、临盆环境的部份 webpack 设置是差别的,比方 publicPath 在临盆环境可以要设置一个 CDN 地点。

我们在根目次竖立一个文件夹 config,内里建立 3 个设置文件:

  • default.js: 临盆环境
module.exports = {
  publicPath: 'http://cdn.example.com/assets/'
}
  • dev.js: 默许开辟环境
module.exports = {
  publicPath: '/assets/',

  serve: {
    port: 8090
  }
}
  • local.js: 个人当地环境,在 dev.js 基本上修正部份参数。
const config = require('./dev')
config.serve.port = 8070
module.exports = config

package.json 修正 scripts:

{
  "scripts": {
    "local": "npm run webpack-serve --config=local",
    "dev": "npm run webpack-serve --config=dev",
    "webpack-serve": "webpack-serve webpack.config.js",
    "build": "webpack-cli"
  }
}

webpack 设置修正:

// ...
const url = require('url')

const config = require('./config/' + (process.env.npm_config_config || 'default'))

module.exports = {
  // ...
  
  output: {
    // ...
    publicPath: config.publicPath
  }
  
  // ...
}

if (dev) {
  module.exports.serve = {
    host: '0.0.0.0',
    port: config.serve.port,
    dev: {
      publicPath: config.publicPath
    },
    add: app => {
      app.use(convert(history({
        index: url.parse(config.publicPath).pathname
      })))
    }
  }
}

这里的关键是 npm run 传进来的自定义参数可以经过历程 process.env.npm_config_* 取得。参数中假如有 - 会被转成 _

另有一点,我们不须要把自身个人用的设置文件提交到 git,所以我们在 .gitignore 中到场:

config/*
!config/default.js
!config/dev.js

config 目次消撤除,然则保存临盆环境和 dev 默许设置文件。

可以有同砚注重到了 webpack-cli 可以经过历程 –env 的体式格局从敕令行传参给剧本,遗憾的是 webpack-cli 不支撑

webpack-serve 处置惩罚带后缀名的文件的特别划定规矩

当处置惩罚带后缀名的要求时,比方 http://localhost:8080/bar.do ,connect-history-api-fallback 会以为它应当是一个现实存在的文件,就算找不到该文件,也不会 fallback 到 index.html,而是返回 404。但在 SPA 运用中这不是我们愿望的。

幸好有一个设置选项 disableDotRule: true 可以禁用这个划定规矩,使带后缀的文件当不存在时也能 fallback 到 index.html

module.exports.serve = {
  // ...
  add: app => {
    app.use(convert(history({
      // ...
      disableDotRule: true,
      htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'] // 须要合营 disableDotRule 一同运用
    })))
  }
}

代码中插进去环境变量

在营业代码中,有些变量在开辟环境和临盆环境是差别的,比方域名、背景 API 地点等。另有开辟环境可以须要打印调试信息等。

我们可以运用 DefinePlugin 插件在打包时往代码中插进去须要的环境变量。

// ...
const pkgInfo = require('./package.json')

module.exports = {
  // ...
  plugins: [
    new webpack.DefinePlugin({
      DEBUG: dev,
      VERSION: JSON.stringify(pkgInfo.version),
      CONFIG: JSON.stringify(config.runtimeConfig)
    }),
    // ...
  ]
}

DefinePlugin 插件的道理很简朴,假如我们在代码中写:

console.log(DEBUG)

它会做相似如许的处置惩罚:

'console.log(DEBUG)'.replace('DEBUG', true)

末了天生:

console.log(true)

这里有一点须要注重,像这里的 VERSION, 假如我们不对 pkgInfo.versionJSON.stringify()

console.log(VERSION)

然后做替代操纵:

'console.log(VERSION)'.replace('VERSION', '1.0.0')

末了天生:

console.log(1.0.0)

如许语法就毛病了。所以,我们须要 JSON.stringify(pkgInfo.version) 转一下变成 '"1.0.0"',替代的时刻才会带引号。

另有一点,webpack 打包紧缩的时刻,会把代码举行优化,比方:

if (DEBUG) {
  console.log('debug mode')
} else {
  console.log('production mode')
}

会被编译成:

if (false) {
  console.log('debug mode')
} else {
  console.log('production mode')
}

然后紧缩优化为:

console.log('production mode')

简化 import 途径

文件 a 引入文件 b 时,b 的途径是相干于 a 文件地点目次的。假如 a 和 b 在差别的目次,藏得又深,写起来就会很贫苦:

import b from '../../../components/b'

为了轻易,我们可以定义一个途径别号(alias):

resolve: {
  alias: {
    '~': resolve(__dirname, 'src')
  }
}

如许,我们可以以 src 目次为基本途径来 import 文件:

import b from '~/components/b'

html 中的 <img> 标签没法运用这个别号功用,但 html-loader 有一个 root 参数,可以使 / 开首的文件相干于 root 目次剖析。

{
  test: /\.html$/,
  use: [
    {
      loader: 'html-loader',
      options: {
        root: resolve(__dirname, 'src'),
        attrs: ['img:src', 'link:href']
      }
    }
  ]
}

那末,<img src="/favicon.png"> 就可以顺遂指向到 src 目次下的 favicon.png 文件,不须要体贴当前文件和目的文件的相对途径。

PS: 在调试 <img> 标签的时刻碰到一个坑,html-loader 会剖析 <!-- --> 解释中的内容,之前在解释中写的

<!--
大于 10kb 的图片,图片会被储存到输出目次,src 会被替代为打包后的途径
<img src="/assets/f78661bef717cf2cc2c2e5158f196384.png">
-->

之前由于没有加 root 参数,所以 / 开首的文件名不会被剖析,加了 root 致使编译时报错,找不到该文件。人人记着这一点。

优化 babel 编译后的代码机能

babel 编译后的代码平常会形成机能丧失,babel 供应了一个 loose 选项,使编译后的代码不须要完整遵照 ES6 划定,简化编译后的代码,进步代码实行效力:

package.json:

{
  "babel": {
    "presets": [
      [
        "env",
        {
          "loose": true
        }
      ],
      "stage-2"
    ]
  }
}

但这么做会有兼容性的风险,可以会致使 ES6 源码理应的实行结果和编译后的 ES5 代码的现实结果并不一致。假如代码没有碰到现实的效力瓶颈,官方 不发起 运用 loose 形式。

运用 webpack 自带的 ES6 模块处置惩罚功用

我们现在的设置,babel 会把 ES6 模块定义转为 CommonJS 定义,但 webpack 自身可以处置惩罚 importexport, 而且 webpack 处置惩罚 import 时会做代码优化,把没用到的部份代码删撤除。因而我们经过历程 babel 供应的 modules: false 选项把 ES6 模块转为 CommonJS 模块的功用给封闭掉。

package.json:

{
  "babel": {
    "presets": [
      [
        "env",
        {
          "loose": true,
          "modules": false
        }
      ],
      "stage-2"
    ]
  }
}

运用 autoprefixer 自动建立 css 的 vendor prefixes

css 有一个很贫苦的题目就是比较新的 css 属性在各个阅读器里是要加前缀的,我们可以运用 autoprefixer 东西自动建立这些阅读器划定规矩,那末我们的 css 中只须要写:

:fullscreen a {
    display: flex
}

autoprefixer 会编译成:

:-webkit-full-screen a {
    display: -webkit-box;
    display: flex
}
:-moz-full-screen a {
    display: flex
}
:-ms-fullscreen a {
    display: -ms-flexbox;
    display: flex
}
:fullscreen a {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex
}

起首,我们用 npm 装置它:

npm install postcss-loader autoprefixer --save-dev

autoprefixer 是 postcss 的一个插件,所以我们也要装置 postcss 的 webpack loader

修正一下 webpack 的 css rule:

{
  test: /\.css$/,
  use: ['style-loader', 'css-loader', 'postcss-loader']
}

然后建立文件 postcss.config.js:

module.exports = {
  plugins: [
    require('autoprefixer')()
  ]
}

运用 webpack 打包多页面运用(Multiple-Page Application)

多页面网站一样可以用 webpack 来打包,以便运用 npm 包,import()code splitting 等优点。

MPA 意味着并没不是一个单一的 html 进口和 js 进口,而是每一个页面对应一个 html 和多个 js。那末我们可以把项目构造设计为:

├── dist
├── package.json
├── node_modules
├── src
│   ├── components
│   ├── shared
|   ├── favicon.png
│   └── pages                 页面放这里
|       ├── foo               编译后天生 http://localhost:8080/foo.html
|       |    ├── index.html
|       |    ├── index.js
|       |    ├── style.css
|       |    └── pic.png
|       └── bar                        http://localhost:8080/bar.html
|           ├── index.html
|           ├── index.js
|           ├── style.css
|           └── baz                    http://localhost:8080/bar/baz.html
|               ├── index.html
|               ├── index.js
|               └── style.css
└── webpack.config.js

这里每一个页面的 index.html 是个完整的从 <!DOCTYPE html> 开首到 </html> 完毕的页面,这些文件都要用 html-webpack-plugin 处置惩罚。index.js 是每一个页面的营业逻辑,作为每一个页面的进口 js 设置到 entry 中。这里我们须要用 glob 库来把这些文件都挑选出来批量操纵。为了运用 webpack 4 的 optimization.splitChunksoptimization.runtimeChunk 功用,我写了 html-webpack-include-sibling-chunks-plugin 插件来合营运用。还要装几个插件把 css 紧缩并放到 <head> 中。

npm install glob html-webpack-include-sibling-chunks-plugin uglifyjs-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin --save-dev

webpack.config.js 修正的处所:

// ...
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackIncludeSiblingChunksPlugin = require('html-webpack-include-sibling-chunks-plugin')
const glob = require('glob')

const dev = Boolean(process.env.WEBPACK_SERVE)
const config = require('./config/' + (process.env.npm_config_config || 'default'))

const entries = glob.sync('./src/**/index.js')
const entry = {}
const htmlPlugins = []
for (const path of entries) {
  const template = path.replace('index.js', 'index.html')
  const chunkName = path.slice('./src/pages/'.length, -'/index.js'.length)
  entry[chunkName] = dev ? [path, template] : path
  htmlPlugins.push(new HtmlWebpackPlugin({
    template,
    filename: chunkName + '.html',
    chunksSortMode: 'none',
    chunks: [chunkName]
  }))
}

module.exports = {
  entry,

  output: {
    path: resolve(__dirname, 'dist'),
    // 我们不定义 publicPath,不然接见 html 时须要带上 publicPath 前缀
    filename: dev ? '[name].js' : '[chunkhash].js',
    chunkFilename: '[chunkhash].js'
  },

  optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: 'all'
    },
    minimizer: dev ? [] : [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: true
      }),
      new OptimizeCSSAssetsPlugin()
    ]
  },

  module: {
    rules: [
      // ...
      
      {
        test: /\.css$/,
        use: [dev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      },
      
      // ...
    ]
  },

  plugins: [
    // ...
    
    /*
    这里不运用 [chunkhash]
    由于从同一个 chunk 抽离出来的 css 同享同一个 [chunkhash]
    [contenthash] 你可以简朴理解为 moduleId + content 天生的 hash
    因而一个 chunk 中的多个 module 有自身的 [contenthash]
    */
    new MiniCssExtractPlugin({
      filename: '[contenthash].css',
      chunkFilename: '[contenthash].css'
    }),

    // 必需放在html-webpack-plugin前面
    new HtmlWebpackIncludeSiblingChunksPlugin(),

    ...htmlPlugins
  ],

  // ...
}

entryhtmlPlugins 会经过历程遍历 pages 目次天生,比方:

entry:

{
  'bar/baz': './src/pages/bar/baz/index.js',
  bar: './src/pages/bar/index.js',
  foo: './src/pages/foo/index.js'
}

在开辟环境中,为了可以修正 html 文件后网页可以自动革新,我们还须要把 html 文件也到场 entry 中,比方:

{
  foo: ['./src/pages/foo/index.js', './src/pages/foo/index.html']
}

如许,当 foo 页面的 index.js 或 index.html 文件修改时,都邑触发阅读器革新该页面。虽然把 html 到场 entry 很新鲜,但宁神,不会致使毛病。记得不要在临盆环境这么做,不然致使 chunk 文件包括了无用的 html 片断。

htmlPlugins:

[
  new HtmlWebpackPlugin({
    template: './src/pages/bar/baz/index.html',
    filename: 'bar/baz.html',
    chunksSortMode: 'none',
    chunks: ['bar/baz']
  },

  new HtmlWebpackPlugin({
    template: './src/pages/bar/index.html',
    filename: 'bar.html',
    chunksSortMode: 'none',
    chunks: ['bar']
  },

  new HtmlWebpackPlugin({
    template: './src/pages/foo/index.html',
    filename: 'foo.html',
    chunksSortMode: 'none',
    chunks: ['foo']
  }
]

代码在 examples/mpa 目次。

总结

经过历程这篇文章,我想人人应当学会了 webpack 的准确翻开姿态。虽然我没有说起怎样用 webpack 来编译 Reactvue.js, 但人人可以想到,无非是装置一些 loader 和 plugin 来处置惩罚 jsxvue 花样的文件,当时难度就不在于 webpack 了,而是代码架构构造的题目了。细致的人人自身去探索一下。

    原文作者:fenivana
    原文地址: https://segmentfault.com/a/1190000007914129
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞