资讯专栏INFORMATION COLUMN

Webpack 4 和单页应用入门

Zoom / 1333人阅读

摘要:但由于和技术过于和复杂,并没能得到广泛的推广。但是在浏览器内并不适用。依托模块化编程,的实现方式更为简单清晰,一个网页不再是传统的类似文档的页面,而是一个完整的应用程序。到了这里,我们的主角登场了年此处应有掌声。和差不多同期登场的还有。

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

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

写在开头

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

和这样的:

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

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

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

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

在很长的一段前端历史里,是不存在打包这个说法的。那个时候页面基本是纯静态的或者服务端输出的,没有 AJAX,也没有 jQuery。那个时候的 JavaScript 就像个玩具,用处大概就是在侧栏弄个时钟,用 media player 放个 mp3 之类的脚本,代码量不是很多,直接放在

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

js 模块化问题基本解决了,css 和 html 也没闲着。什么 less,sass,stylus 的 css 预处理器横空出世,说能帮我们简化 css 的写法,自动给你加 vendor prefix。html 在这期间也出现了一堆模板语言,什么 handlebars,ejs,jade,可以把 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 的 一般是空的,或者只有总的布局(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 支持分模块打包,但配置很麻烦,因为模块之间会互相依赖,在配置的时候需要 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 需要安装 linter 和 linter-eslint 这两个插件,装好后重启生效。

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

写几个页面

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



  
    
  

  
  

它是一个空白页面,注意这里我们不需要自己写 , 因为打包后的文件名和路径可能会变,所以我们用 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, 会生成