# 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://github.com/fenivana/webpack-and-spa-guide/raw/master/assets/webpack.png) ## 写在开头 ~~先说说为什么要写这篇文章,最初的原因是组里的小朋友们看了 [webpack](https://webpack.js.org/) 文档后,表情都是这样的:摘自 webpack 一篇文档的评论区)~~ ![WTF](https://github.com/fenivana/webpack-and-spa-guide/raw/master/assets/wtf.jpg) ~~和这样的:~~ ![You Couldn't Handle Me](https://github.com/fenivana/webpack-and-spa-guide/raw/master/assets/couldn't-handle.jpg) ~~是的,即使是外国佬也在吐槽这文档不是人能看的。回想起当年自己啃 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),比如下图: ![layout](https://github.com/fenivana/webpack-and-spa-guide/raw/master/assets/layout.png) 布局会把 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 开关: ![WebStorm ESLint Config](https://github.com/fenivana/webpack-and-spa-guide/raw/master/assets/webstorm-eslint-config.png) ### 写几个页面 我们写一个最简单的 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, 会生成