总结包括:
1 了解前端打包方案的黑暗历史
2 模块化发展
3 Webpack3 打包流程
在说webpack之前, 我想先谈一下前端打包方案这几年的演进历程, 在什么场景下, 我们遇到了什么问题, 催生出了应对这些问题的工具. 了解了需求和目的之后, 你就知道什么时候webpack可以帮到你。
参考 http://javascript.ruanyifeng.com/introduction/history.html
1990年底,欧洲核能研究组织科学家发明了万维网(World Wide Web),从此可以在网上浏览网页文件。
1992年底,美国国家超级电脑应用中心(NCSA)开发人类历史上第一个浏览器,从此网页可以在图形界面的窗口浏览。
1994年12月,Navigator发布了1.0版,市场份额一举超过90%。公司很快发现,浏览器需要一种可以嵌入网页的脚本语言,用来控制浏览器行为。当时,网速很慢而且上网费很贵,有些操作不宜在服务器端完成。比如,如果用户忘记填写“用户名”,就点了“发送”按钮,到服务器再发现这一点就有点太晚了,最好能在用户发出数据之前,就告诉用户“请填写用户名”。这就需要在网页中嵌入小程序,让浏览器检查每一栏是否都填写了。
管理层对这种浏览器脚本语言的设想是:功能不需要太强,语法较为简单,容易学习和部署。那一年,正逢Java语言问世,市场推广活动非常成功。Netscape公司决定与Sun公司合作,浏览器支持嵌入Java小程序。但是后来,还是决定不使用Java,因为网页小程序不需要Java这么“重”的语法。
1995年5月,Brendan Eich只用了10天,就设计完成了这种语言的第一版。它是一个大杂烩,语法有多个来源:
基本语法:借鉴C语言和Java语言。
数据结构:借鉴Java语言,包括将值分成原始值和对象两大类。
函数的用法:借鉴Scheme语言和Awk语言,将函数当作第一等公民,并引入闭包。
原型继承模型:借鉴Self语言(Smalltalk的一种变种)。
正则表达式:借鉴Perl语言。
字符串和数组处理:借鉴Python语言。
为了保持简单,这种脚本语言缺少一些关键的功能,比如块级作用域、模块、子类型(subtyping)等,但可以利用现有功能找出解决办法。这直接导致了后来JavaScript的一个特点:对于其他语言,你需要学习语言的各种功能,而对于JavaScript,你常常需要学习各种解决问题的模式。而且由于来源多样,从一开始就注定,JavaScript的编程风格是函数式编程和面向对象编程的一种混合体。
1997年7月,ECMAScript 1.0发布。
1998年6月,ECMAScript 2.0版发布。
1999年12月,ECMAScript 3.0版发布,成为JavaScript的通行标准,得到了广泛支持。
2007年10月,ECMAScript 4.0版草案发布,对3.0版做了大幅升级,预计次年8月发布正式版本。草案发布后,由于4.0版的目标过于激进,各方对于是否通过这个标准,发生了严重分歧。以Yahoo、Microsoft、Google为首的大公司,反对JavaScript的大幅升级,主张小幅改动;以JavaScript创造者Brendan Eich为首的Mozilla公司,则坚持当前的草案。
2009年12月,ECMAScript 5.0版正式发布。
2015年6月,ECMAScript 6正式发布。
实际上 javascript可以说是一个非常好用和十分优美的语言
并且学习成本相对其它语言低的多
我认为它的整体设计 其实是非常成功的
想像一下一堆人围着超市的手推车 抱怨它不好用的场景吧
有人说 为什么里面没有设计格子
有人说 为什么没有设计刹车
有人说 为什么没有设计档位
......
手推车就是手推车 它已经完美的完成了它的工作
2006年,jQuery函数库为操作网页DOM结构提供了非常强大易用的接口,成为了使用最广泛的函数库,并且让JavaScript语言的应用难度大大降低,推动了这种语言的流行。
2009年,Node.js项目诞生,创始人为Ryan Dahl,它标志着JavaScript可以用于服务器端编程,从此网站的前端和后端可以使用同一种语言开发。并且,Node.js可以承受很大的并发流量,使得开发某些互联网大规模的实时应用变得容易。
2010年,三个重要的项目诞生,分别是NPM、BackboneJS和RequireJS,标志着JavaScript进入模块化开发的时代。
2012年,单页面应用程序框架(single-page app framework)开始崛起,AngularJS项目和Ember项目都发布了1.0版本。
2013年5月,Facebook发布UI框架库React,引入了新的JSX语法,使得UI层可以用组件开发。
2015年3月,Facebook公司发布了React Native项目,将React框架移植到了手机端,可以用来开发手机App。它会将JavaScript代码转为iOS平台的Objective-C代码,或者Android平台的Java代码,从而为JavaScript语言开发高性能的原生App打开了一条道路。
2015年4月,Angular框架宣布,2.0版将基于微软公司的TypeScript语言开发,这等于为JavaScript语言引入了强类型。
2017年11月,所有主流浏览器全部支持 WebAssembly,这意味着任何语言都可以编译成 JavaScript,在浏览器运行。
1995年到2005年,前端是不存在打包这个说法的。那时候页面基本是静态的或者是服务端输出的,JavaScript 代码量不是很多, 直接放在
以上是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, 图片等资源。
然后2012年Grunt产生了, 配置文件格式是我们最爱的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更容易地实现, 但小问题还是很多的:
1 不是所有的第三方库都是AMD规范的, 这时候要配置shim, 很麻烦。
2 html里面的的路径是个问题, 需要使用绝对路径并且保持打包后的图片路径和打包前的路径不变, 或者使用html模板语言把src写成变量, 在运行时生成。
3 不支持动态加载css, 变通的方法是把所有的css文件合并压缩成一个文件, 在入口的html页面一次性加载。
4 SPA项目越做越大, 一个应用打包后的js文件到了几MB的大小. 虽然r.js支持分模块打包, 但配置很麻烦, 因为模块之间会互相依赖, 在配置的时候需要exclude那些通用的依赖项, 而依赖项要在文件里一个个检查。
5 所有的第三方库都要自己一个个的下载, 解压, 放到某个目录下, 更别提更新有多麻烦了。 虽然可以用npm包管理工具, 但npm的包都是CommonJS规范的, 给后端Node.js用的, 只有部分支持AMD规范。
6 AMD规范定义和引用模块的语法太麻烦, 上面介绍的AMD语法仅是最简单通用的语法, API文档里面还有很多变异的写法, 特别是当发生循环引用的时候(a依赖b, b依赖a), 需要使用其他的语法解决这个问题。
7 项目的文件结构不合理, 因为grunt/gulp是按照文件格式批量处理的, 所以一般会把js, html, css, 图片分别放在不同的目录下, 所以同一个模块的文件会散落在不同的目录下, 开发的时候找文件是个麻烦的事情。 code review时想知道一个文件是哪个模块的也很麻烦, 解决办法比如又要在imgs目录下建立按模块命名的文件夹, 里面再放图片。
到了这里, 我们的主角webpack登场了(2012年)(此处应有掌声)
参考 https://zhuanlan.zhihu.com/p/27046322
http://www.ruanyifeng.com/blog/2012/10/javascript_module.html
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
安装
npm install --save-dev webpack
npm install --save-dev webpack@
webpack 通过运行一个或多个 npm scripts,会在本地 node_modules
目录中查找安装的 webpack:
"scripts": {
"start": "webpack --config webpack.config.js"
}
“源”代码是用于书写和编辑的代码。“分发”代码是构建过程产生的代码最小化和优化后的“输出”目录,最终将在浏览器中加载。
ES2015 中的 import
和 export
语句已经被标准化。虽然大多数浏览器还无法支持它们,但是 webpack 却能够提供开箱即用般的支持。
大多数项目会需要很复杂的设置,这就是为什么 webpack 要支持配置文件。这比在终端(terminal)中输入大量命令要高效的多,所以让我们创建一个取代以上使用 CLI 选项方式的配置文件webpack.config.js。
考虑到用 CLI 这种方式来运行本地的 webpack 不是特别方便,我们可以设置一个快捷方式。在 package.json添加一个 npm 脚本(npm script):
{
...
"scripts": {
"build": "webpack"
},
...
}
webpack 最出色的功能之一就是,除了 JavaScript,还可以通过 loader 引入任何其他类型的文件。也就是说,以上列出的那些 JavaScript 的优点(例如显式依赖),同样可以用来构建网站或 web 应用程序中的所有非 JavaScript 内容。
npm install --save-dev style-loader css-loader file-loader
csv-loader xml-loader
如果我们更改了我们的一个入口起点的名称,甚至添加了一个新的名称,会发生什么?生成的包将被重命名在一个构建中,但是我们的index.html
文件仍然会引用旧的名字。我们用 HtmlWebpackPlugin
来解决这个问题。
npm install --save-dev html-webpack-plugin
通常,在每次构建前清理 /dist
文件夹,是比较推荐的做法,因此只会生成用到的文件。让我们完成这个需求。
clean-webpack-plugin
是一个比较普及的管理插件。
为了更容易地追踪错误和警告,JavaScript 提供了 source map 功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js
,source map 就会明确的告诉你。
webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:
webpack's Watch Mode
webpack-dev-server
webpack-dev-middleware
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --progress --watch",
"start": "webpack-dev-server --open",
"build": "webpack"
},
开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
UglifyJSPlugin
是代码压缩方面比较好的选择,但是还有一些其他可选择项。
import 'jQuery'
import 'bootstrap/dist/css/bootstrap.min.css'
import './css/common.css'
import img from './img/top_left.png'
import moment from 'moment'
window.moment = moment
"scripts": {
"start": "webpack-dev-server --open --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},
const path = require('path');
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin")
const CleanWebpackPlugin = require("clean-webpack-plugin")
module.exports = {
entry: {
app: './src/index.js',
},
devtool: 'inline-source-map',
devServer: {
contentBase: "./dist"
},
plugins: [
new CleanWebpackPlugin(["dist"]),
new HtmlWebpackPlugin({
title: "Output Management",
template: './public/index.html'
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}
]
}
};