摘要:但由于和技术过于和复杂,并没能得到广泛的推广。但是在浏览器内并不适用。依托模块化编程,的实现方式更为简单清晰,一个网页不再是传统的类似文档的页面,而是一个完整的应用程序。到了这里,我们的主角登场了年此处应有掌声。和差不多同期登场的还有。
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 之类的脚本,代码量不是很多,直接放在 标签里或者弄个 js 文件引一下就行,日子过得很轻松愉快。
随后的几年,人们开始尝试在一个页面里做更多的事情。容器的显示,隐藏,切换。用 css 写的弹层,图片轮播等等。但如果一个页面内不能向服务器请求数据,能做的事情毕竟有限的,代码的量也能维持在页面交互逻辑范围内。这时候很多人开始突破一个页面能做的事情的范围,使用隐藏的 iframe 和 flash 等作为和服务器通信的桥梁,新世界的大门慢慢地被打开,在一个页面内和服务器进行数据交互,意味着以前需要跳转多个页面的事情现在可以用一个页面搞定。但由于 iframe 和 flash 技术过于 tricky 和复杂,并没能得到广泛的推广。
直到 Google 推出 Gmail 的时候(2004 年),人们意识到了一个被忽略的接口,XMLHttpRequest, 也就是我们俗称的 AJAX, 这是一个使用方便的,兼容性良好的服务器通信接口。从此开始,我们的页面开始玩出各种花来了,前端一下子出现了各种各样的库,Prototype、Dojo、MooTools、Ext JS、jQuery…… 我们开始往页面里插入各种库和插件,我们的 js 文件也就爆炸了。
随着 js 能做的事情越来越多,引用越来越多,文件越来越大,加上当时大约只有 2Mbps 左右的网速,下载速度还不如 3G 网络,对 js 文件的压缩和合并的需求越来越强烈,当然这里面也有把代码混淆了不容易被盗用等其他因素在里面。JSMin、YUI Compressor、Closure Compiler、UglifyJS 等 js 文件压缩合并工具陆陆续续诞生了。压缩工具是有了,但我们得要执行它,最简单的办法呢,就是 windows 上搞个 bat 脚本,mac / linux 上搞个 bash 脚本,哪几个文件要合并在一块的,哪几个要压缩的,发布的时候运行一下脚本,生成压缩后的文件。
基于合并压缩技术,项目越做越大,问题也越来越多,大概就是以下这些问题:
库和插件为了要给他人调用,肯定要找个地方注册,一般就是在 window 下申明一个全局的函数或对象。难保哪天用的两个库在全局用同样的名字,那就冲突了。
库和插件如果还依赖其他的库和插件,就要告知使用人,需要先引哪些依赖库,那些依赖库也有自己的依赖库的话,就要先引依赖库的依赖库,以此类推。
恰好就在这个时候(2009 年),随着后端 JavaScript 技术的发展,人们提出了 CommonJS 的模块化规范,大概的语法是: 如果 a.js 依赖 b.js 和 c.js, 那么就在 a.js 的头部,引入这些依赖文件:
var b = require("./b") var c = require("./c")
那么变量 b 和 c 会是什么呢?那就是 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:
以上是 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.jswebpack 是基于我大 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 中的 devDependencies 和 dependencies 字段,把记录的包的相应版本下载下来。
这里 eslint-config-enough 是配置文件,它规定了代码规范,要使它生效,我们要在 package.json 中添加内容:
{ "eslintConfig": { "extends": "enough", "env": { "browser": true, "node": true } } }
业界最有名的语法规范是 airbnb 出品的,但它规定的太死板了,比如不允许使用 for-of 和 for-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, 会生成