# webpack-and-spa-guide
**Repository Path**: sickle12138/webpack-and-spa-guide
## Basic Information
- **Project Name**: webpack-and-spa-guide
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2020-03-12
- **Last Updated**: 2024-11-29
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Webpack 4 和单页应用入门
> webpack 更新到了 4.0,官网还没有更新文档。因此把教程更新一下,方便大家用起 webpack 4。

## 写在开头
~~先说说为什么要写这篇文章,最初的原因是组里的小朋友们看了 [webpack](https://webpack.js.org/) 文档后,表情都是这样的:摘自 webpack 一篇文档的评论区)~~

~~和这样的:~~

~~是的,即使是外国佬也在吐槽这文档不是人能看的。回想起当年自己啃 webpack 文档的血与泪的往事,觉得有必要整一个教程,可以让大家看完后愉悦地搭建起一个 webpack 打包方案的项目。~~
官网新的 [webpack](https://webpack.js.org/) 文档现在写的很详细了,能看英文的小伙伴可以直接去看官网。
可能会有人问 webpack 到底有什么用,你不能上来就糊我一脸代码让我马上搞,我照着搞了一遍结果根本没什么用,都是骗人的。所以,在说 webpack 之前,我想先谈一下前端打包方案这几年的演进历程,在什么场景下,我们遇到了什么问题,催生出了应对这些问题的工具。了解了需求和目的之后,你就知道什么时候 webpack 可以帮到你。我希望我用完之后很爽,你们用完之后也是。
## 先说说前端打包方案的黑暗历史
在很长的一段前端历史里,是不存在打包这个说法的。那个时候页面基本是纯静态的或者服务端输出的,没有 AJAX,也没有 jQuery。那个时候的 JavaScript 就像个玩具,用处大概就是在侧栏弄个时钟,用 media player 放个 mp3 之类的脚本,代码量不是很多,直接放在 `
```
以上是 AMD 规范的基本用法,更详细的就不多说了(反正也淘汰了~),有兴趣的可以看 [这里](http://requirejs.org/docs/api.html)。
js 模块化问题基本解决了,css 和 html 也没闲着。什么 [less](http://lesscss.org/),[sass](http://sass-lang.com/),[stylus](http://stylus-lang.com/) 的 css 预处理器横空出世,说能帮我们简化 css 的写法,自动给你加 vendor prefix。html 在这期间也出现了一堆模板语言,什么 [handlebars](http://handlebarsjs.com/),[ejs](http://www.embeddedjs.com/),[jade](http://jade-lang.com/),可以把 ajax 拿到的数据插入到模板中,然后用 innerHTML 显示到页面上。
托 AMD 和 CSS 预处理和模板语言的福,我们的编译脚本也洋洋洒洒写了百来行。命令行脚本有个不好的地方,就是 windows 和 mac/linux 是不通用的,如果有跨平台需求的话,windows 要装个可以执行 bash 脚本的命令行工具,比如 msys(目前最新的是 [msys2](http://msys2.github.io/)),或者使用 php 或 python 等其他语言的脚本来编写,对于非全栈型的前端程序员来说,写 bash / php / python 还是很生涩的。因此我们需要一个简单的打包工具,可以利用各种编译工具,编译 / 压缩 js、css、html、图片等资源。然后 [Grunt](http://gruntjs.com/) 产生了(2012 年),配置文件格式是我们最爱的 js,写法也很简单,社区有非常多的插件支持各种编译、lint、测试工具。一年多后另一个打包工具 [gulp](http://gulpjs.com/) 诞生了,扩展性更强,采用流式处理效率更高。
依托 AMD 模块化编程,SPA(Single-page application) 的实现方式更为简单清晰,一个网页不再是传统的类似 word 文档的页面,而是一个完整的应用程序。SPA 应用有一个总的入口页面,我们通常把它命名为 index.html、app.html、main.html,这个 html 的 `
` 一般是空的,或者只有总的布局(layout),比如下图:

布局会把 header、nav、footer 的内容填上,但 main 区域是个空的容器。这个作为入口的 html 最主要的工作是加载启动 SPA 的 js 文件,然后由 js 驱动,根据当前浏览器地址进行路由分发,加载对应的 AMD 模块,然后该 AMD 模块执行,渲染对应的 html 到页面指定的容器内(比如图中的 main)。在点击链接等交互时,页面不会跳转,而是由 js 路由加载对应的 AMD 模块,然后该 AMD 模块渲染对应的 html 到容器内。
虽然 AMD 模块让 SPA 更容易地实现,但小问题还是很多的:
* 不是所有的第三方库都是 AMD 规范的,这时候要配置 `shim`,很麻烦。
* 虽然 RequireJS 支持通过插件把 html 作为依赖加载,但 html 里面的 `
` 的路径是个问题,需要使用绝对路径并且保持打包后的图片路径和打包前的路径不变,或者使用 html 模板语言把 `src` 写成变量,在运行时生成。
* 不支持动态加载 css,变通的方法是把所有的 css 文件合并压缩成一个文件,在入口的 html 页面一次性加载。
* SPA 项目越做越大,一个应用打包后的 js 文件到了几 MB 的大小。虽然 [r.js](http://requirejs.org/docs/optimization.html) 支持分模块打包,但配置很麻烦,因为模块之间会互相依赖,在配置的时候需要 exclude 那些通用的依赖项,而依赖项要在文件里一个个检查。
* 所有的第三方库都要自己一个个的下载,解压,放到某个目录下,更别提更新有多麻烦了。虽然可以用 [npm](https://www.npmjs.com/) 包管理工具,但 npm 的包都是 CommonJS 规范的,给后端 Node.js 用的,只有部分支持 AMD 规范,而且在 npm 3 之前,这些包有依赖项的话也是不能用的。后来有个 [bower](https://bower.io/) 包管理工具是专门的 web 前端仓库,这里的包一般都支持 AMD 规范。
* AMD 规范定义和引用模块的语法太麻烦,上面介绍的 AMD 语法仅是最简单通用的语法,API 文档里面还有很多变异的写法,特别是当发生循环引用的时候(a 依赖 b,b 依赖 a),需要使用其他的 [语法](http://requirejs.org/docs/api.html#circular) 解决这个问题。而且 npm 上很多前后端通用的库都是 CommonJS 的语法。后来很多人又开始尝试使用 ES6 模块规范,如何引用 ES6 模块又是一个大问题。
* 项目的文件结构不合理,因为 grunt/gulp 是按照文件格式批量处理的,所以一般会把 js、html、css、图片分别放在不同的目录下,所以同一个模块的文件会散落在不同的目录下,开发的时候找文件是个麻烦的事情。code review 时想知道一个文件是哪个模块的也很麻烦,解决办法比如又要在 imgs 目录下建立按模块命名的文件夹,里面再放图片。
到了这里,我们的主角 webpack 登场了(2012 年)(此处应有掌声)。
和 webpack 差不多同期登场的还有 [Browserify](http://browserify.org/)。这里简单介绍一下 Browserify。Browserify 的目的是让前端也能用 CommonJS 的语法 `require('module')` 来加载 js。它会从入口 js 文件开始,把所有的 `require()` 调用的文件打包合并到一个文件,这样就解决了异步加载的问题。那么 Browserify 有什么不足之处导致我不推荐使用它呢? 主要原因有下面几点:
* 最主要的一点,Browserify 不支持把代码打包成多个文件,在有需要的时候加载。这就意味着访问任何一个页面都会全量加载所有文件。
* Browserify 对其他非 js 文件的加载不够完善,因为它主要解决的是 `require()` js 模块的问题,其他文件不是它关心的部分。比如 html 文件里的 img 标签,它只能转成 [Data URI](https://en.wikipedia.org/wiki/Data_URI_scheme) 的形式,而不能替换为打包后的路径。
* 因为上面一点 Browserify 对资源文件的加载支持不够完善,导致打包时一般都要配合 gulp 或 grunt 一块使用,无谓地增加了打包的难度。
* Browserify 只支持 CommonJS 模块规范,不支持 AMD 和 ES6 模块规范,这意味旧的 AMD 模块和将来的 ES6 模块不能使用。
基于以上几点,Browserify 并不是一个理想的选择。那么 webpack 是否解决了以上的几个问题呢? 废话,不然介绍它干嘛。那么下面章节我们用实战的方式来说明 webpack 是怎么解决上述的问题的。
## 上手先搞一个简单的 SPA 应用
一上来步子太大容易扯到蛋,让我们先弄个最简单的 webpack 配置来热一下身。
### 安装 Node.js
webpack 是基于我大 Node.js 的打包工具,上来第一件事自然是先安装 Node.js 了,[传送门 ->](https://nodejs.org/)。
### 初始化一个项目
我们先随便找个地方,建一个文件夹叫 `simple`, 然后在这里面搭项目。完成品在 [examples/simple](https://github.com/fenivana/webpack-and-spa-guide/blob/master/examples/simple) 目录,大家搞的时候可以参照一下。我们先看一下目录结构:
```
├── dist 打包输出目录,只需部署这个目录到生产环境
├── package.json 项目配置信息
├── node_modules npm 安装的依赖包都在这里面
├── src 我们的源代码
│ ├── components 可以复用的模块放在这里面
│ ├── index.html 入口 html
│ ├── index.js 入口 js
│ ├── shared 公共函数库
│ └── views 页面放这里
└── webpack.config.js webpack 配置文件
```
打开命令行窗口,`cd` 到刚才建的 simple 目录。然后执行这个命令初始化项目:
```sh
npm init
```
命令行会要你输入一些配置信息,我们这里一路按回车下去,生成一个默认的项目配置文件 `package.json`。
### 给项目加上语法报错和代码规范检查
我们安装 [eslint](http://eslint.org/), 用来检查语法报错,当我们书写 js 时,有错误的地方会出现提示。
```sh
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 目录的,这时我们需要运行
```sh
npm install
```
它会读取 `package.json` 中的 `devDependencies` 和 `dependencies` 字段,把记录的包的相应版本下载下来。
这里 [eslint-config-enough](https://github.com/fenivana/eslint-config-enough) 是配置文件,它规定了代码规范,要使它生效,我们要在 `package.json` 中添加内容:
```json
{
"eslintConfig": {
"extends": "enough",
"env": {
"browser": true,
"node": true
}
}
}
```
业界最有名的语法规范是 [airbnb](https://github.com/airbnb/javascript) 出品的,但它规定的太死板了,比如不允许使用 `for-of` 和 `for-in` 等。感兴趣的同学可以参照 [这里](https://www.npmjs.com/package/eslint-config-airbnb) 安装使用。
[babel-eslint](https://github.com/babel/babel-eslint) 是 `eslint-config-enough` 依赖的语法解析库,替代 eslint 默认的解析库以支持还未标准化的语法。比如 [import()](https://github.com/tc39/proposal-dynamic-import)。
[eslint-loader](https://github.com/MoOx/eslint-loader) 用于在 webpack 编译的时候检查代码,如果有错误,webpack 会报错。
项目里安装了 eslint 还没用,我们的 IDE 和编辑器也得要装 eslint 插件支持它。
[Visual Studio Code](https://code.visualstudio.com/) 需要安装 [ESLint 扩展](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
[atom](https://atom.io/) 需要安装 [linter](https://atom.io/packages/linter) 和 [linter-eslint](https://atom.io/packages/linter-eslint) 这两个插件,装好后重启生效。
[WebStorm](https://www.jetbrains.com/webstorm/) 需要在设置中打开 eslint 开关:

### 写几个页面
我们写一个最简单的 SPA 应用来介绍 SPA 应用的内部工作原理。首先,建立 src/index.html 文件,内容如下:
```html
```
它是一个空白页面,注意这里我们不需要自己写 ``, 因为打包后的文件名和路径可能会变,所以我们用 webpack 插件帮我们自动加上。
src/index.js:
```js
// 引入 router
import router from './router'
// 启动 router
router.start()
```
src/router.js:
```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:
```js
// 引入 router
import router from '../../router'
// 引入 html 模板,会被作为字符串引入
import template from './index.html'
// 引入 css, 会生成