我爱编程

2018-01-05

2018-01-05  本文已影响0人  NOTEBOOK2

总结包括:
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 代码量不是很多, 直接放在 <script> 标签里或者弄个js文件引一下就行。随后人们尝试在一个页面做更多事情,比如容器的显示隐藏和切换,用CSS写弹层和图片轮播,用隐藏的iframe和flash等作为和服务器通信的桥梁等,但由于iframe和flash技术过于复杂, 并没能得到广泛的推广。

早在1999年微软发布IE5就引入了新功能:允许JavaScript脚本向服务器发起HTTP请求。这个功能当时并没有引起注意,直到2004年谷歌邮箱和2005年谷歌地图发布,才引起广泛重视。

2005年2月,AJAX这个词第一次正式提出,指围绕这个功能进行开发的一整套做法。概括起来,就是一句话,AJAX通过原生的 XMLHttpRequest 对象发出HTTP请求,得到服务器返回的数据后,再进行处理。它几乎成了新一代网站的标准做法,促成了Web 2.0时代的来临。从此,AJAX成为脚本发起HTTP通信的代名词,W3C也在2006年发布了它的国际标准。

从此, 我们的页面开始玩出各种花来了, 前端一下子出现了各种各样的库:

Prototype JavaScript framework 
Dojo Toolkit
MooTools
Sencha Ext JS
jQuery……

我们开始往页面里插入各种库和插件, js文件也就爆炸了。因为文件越来越多越来越大但是网速很慢,所以就需要各种压缩合并工具。执行压缩工具最简单的办法就是windows上搞个bat脚本, mac/linux上搞个bash脚本, 哪几个文件要合并在一块的, 哪几个要压缩的, 发布时运行一下脚本, 生成压缩后的文件。

基于合并压缩技术, 项目越做越大, 问题也越来越多: 比如使用多个库的时候可能产生命名冲突;库和插件如果还依赖其他的库和插件, 就要告知使用人, 需要先引哪些依赖库, 那些依赖库也有自己的依赖库的话, 就要先引依赖库的依赖库, 以此类推..……

参考 模块的写法http://www.ruanyifeng.com/blog/2012/10/javascript_module.html
随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑。

Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。

但是,Javascript不是一种模块化编程语言,它不支持""(class),更遑论"模块"(module)了。(ES6正式支持"类"和"模块",但还需要很长时间才能投入实用。)

1 原始写法

  function m1(){ }

  function m2(){ }

函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。这种做法的缺点很明显:"污染"了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。

2 对象写法

为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

 var module1 = new Object({

    _count : 0,

    m1 : function (){ },

    m2 : function (){ }

  });

使用的时候,就调用这个对象的属性。

 module1.m1();

但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

module1._count = 5;

3、立即执行函数写法

使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。

  var module1 = (function(){

    var _count = 0;

    var m1 = function(){ };

    var m2 = function(){ };

    return {
      m1 : m1,
      m2 : m2
    };

  })();

独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

这方面更多的讨论,参见Ben Cherry的著名文章《JavaScript Module Pattern: In-Depth》。http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html

接下来将讨论如何在浏览器环境组织不同的模块、管理模块之间的依赖性。

有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。但是,这样做有一个前提,那就是大家必须以同样的方式编写模块。通行的Javascript模块规范共有两种:CommonJS和AMD。主要介绍AMD,但是要先从CommonJS 讲起。

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require( ),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

var math = require('math');

有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

但是CommonJS在浏览器内并不适用。因为require()的返回是同步的, 意味着有多个依赖的话需要一个一个依次下载, 堵塞了js脚本的执行. 所以人们就在CommonJS的基础上定义了Asynchronous Module Definition (AMD)规范(2011年), 使用了异步回调的语法来并行下载多个依赖项。

AMD 意思是异步模块定义。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD也采用require( )语句加载模块,但是不同于CommonJS,它要求两个参数:

require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。

require(['math'], function (math) {
    math.add(2, 3);
  });

require.js的诞生,解决两个问题:
(1)实现js文件的异步加载,避免网页失去响应;
(2)管理模块之间的依赖性,便于代码的编写和维护。
require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

define()和require()的区别是, define()必须要在回调函数中返回一个值作为导出的东西, require()不需要导出东西, 因此回调函数中不需要返回值, 也无法作为被依赖项被其他文件导入, 因此一般用于入口文件。
比如页面中这样加载a.js:

<script src="js/require.js" data-main="js/a"></script>

以上是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的<body>一般是空的, 或者只有总的布局(layout), 比如下图:


image

布局会把header, nav, footer的内容填上, 但main区域是个空的容器。这个作为入口的html最主要的工作是加载启动SPA的js文件, 然后由js驱动, 根据当前浏览器地址进行路由分发, 加载对应的AMD模块, 然后该AMD模块执行, 渲染对应的html到页面指定的容器内(比如图中的main)。在点击链接等交互时, 页面不会跳转, 而是由js路由加载对应的AMD模块, 然后该AMD模块渲染对应的html到容器内。

虽然AMD模块让SPA更容易地实现, 但小问题还是很多的:
1 不是所有的第三方库都是AMD规范的, 这时候要配置shim, 很麻烦。
2 html里面的<img>的路径是个问题, 需要使用绝对路径并且保持打包后的图片路径和打包前的路径不变, 或者使用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@<version>

webpack 通过运行一个或多个 npm scripts,会在本地 node_modules 目录中查找安装的 webpack:

"scripts": {
    "start": "webpack --config webpack.config.js"
}

“源”代码是用于书写和编辑的代码。“分发”代码是构建过程产生的代码最小化和优化后的“输出”目录,最终将在浏览器中加载。

ES2015 中的 importexport 语句已经被标准化。虽然大多数浏览器还无法支持它们,但是 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'
        ]
      }
    ]
  }
};
屏幕快照 2018-01-02 22.14.08.png
上一篇下一篇

猜你喜欢

热点阅读