Webpack 基础

243 阅读8分钟

本次学习到的内容:

  1. webpack产生的背景
  2. 理解前端的模块化、模块化演变的过程
  3. 理解webpack打包的核心思路
  4. webpack核心:loader
  5. webpack核心:plugin

1 webpack是什么

webpack是一个前端项目构建工具(打包工具),一个现代JavaScript应用程序的静态模块打包器

默认:只对js进行处理,其他类型文件需要配置loader或者插件进行处理。

打包:将各个依赖文件进行梳理打包,形成一个JS依赖文件。

2 webpack产生的背景

首先,为什么打包?因为:

  • 各个依赖文件的关系难以梳理,耦合程度较高,代码难以维护
  • 静态资源请求效率低,同时请求多个资源会造成程序运行缓慢
  • 逻辑多、文件多,项目复杂度提高
  • 浏览器对高级javascript支持不友好

然后,为什么要用webpack?因为:

  • 提供了友好的模块化支持
  • 性能优化,把所有依赖包都打包成为一个js文件(bundle.js)文件,会有效降低文件请求次数,一定程度提升性能。
  • 强大而灵活,plugin可插拔。
  • webpack除提供上述功能外,还充当了“翻译官”的角色,处理js兼容问题,例如将ES6翻译为低版本的语法,将less、sass翻译为css等功能。

3 先理解下前端模块化

类似于一个公司由各个部门组成,一个工程也由各个模块组成,高内聚低耦合(不懂内聚耦合的看我另一篇博客),各司其职。先理解一下概念:

3-1 作用域

定义:运行时变量、函数、对象可访问性 作用域决定了代码中变量和其他资源的可见性

全局作用域:

    var a = 1;
    window.a; // 1
    global.a; // 1

局部作用域:

  function a(){
        var v = 1;
    }
    window.v; // undefined

如果在传统js写法中,引入多个script,如果在其中都定义了一个同名变量,就很容易造成全局作用域冲突而导致不可预测的问题:

<body>
    	<script scr="./moduleA.js"></sciprt>
        <script scr="./moduleB.js"></sciprt>
        <script scr="./moduleC.js"></sciprt>
</body>

改进步骤一,使用变量作用域形成局部作用域(避免了全局作用域冲突):

// 定义模块内的局部作用域,以moduleA为例
    var Susan = {
    	name: "susan",
        sex: "female",
        tell: function(){
        	console.log("im susan")
        }
    }

但是步骤一无法保证模块属性内部安全性,比如可能不小心改掉属性值,可以通过立即执行函数进行改写,形成闭包。

那么可以进行改进步骤二:

ps:
什么是立即执行函数?可以点击这里进行参考:
立即执行函数
什么是自由变量?简单来说是跨作用域的变量,可以点击这里进行参考。(里面有一个句很好的知识点:创建这个函数的时候,这个函数的作用域就已经决定了,而是不是在调用的时候。
如果对上面某些概念不理解的话可以点击这里看我另外的博客进行参考:
作用域
闭包

var susanModule = (function() {    
        var name = "susan"
        var sex = "female"
    return {
        tell: function() {
        	// ...逻辑代码
	  console.log('名字', name)
          console.log('性别', sex)
        }
    }    
})();

susanModule.tell();

对于步骤二还有一种写法,推荐使用这种写法,也是早期模块实现的方法:

 (function (window) {
            var name = 'zgc'
            var sex = 'nan'
            function tell() {
                console.log('名字', name)
                console.log('性别', sex)
            }
            window.susanmodule = { tell }
        })(window)

调用验证:

window.susanModule.tell(); //im susan

3-2 模块化的优点

  • 模块化的封装
  • 重用性
  • 解除耦合

3-3 模块化方案进化史

随着模块化优势体现,开发者更倾向于使用模块化协同开发项目,于是在发展过程中形成了很多规范:AMD、COMMONJS、ES6 MODULE

AMD:
Asynchronous Module Defineition (异步模块定义)

优点:

  • 显示出当前模块所依赖的其他模块有哪些。
  • 不用绑定到全局对象上,更近一步增加模块安全性,不担心在其他地方被篡改。
// 求和模块的定义
// define(当前函数ID,当前模块依赖,函数/对象)  函数:利用函数的返回值,导出模块的接口 对象:对象本身就是模块导出值
define('getSum',['math'],function(math) {
    return function(a,b) {
        console.log('sum:'+ math.sum(a,b));
    }
});

CommonJS
最初为了解决服务端的模块化标准,后 Node.js 采用并实现其规范。

在 CommonJS 中,每个文件就是一个模块,并且拥有他自己的作用域和上下文,模块的依赖通过 require 函数引入,通过 exports 将其导出。

同 AMD 一样,强调依赖的显示,方便维护复杂模块时,不用操心各个模块的引入顺序。

// 引入
const math = require('./math');
// 导出
exports.getNum = function(a,b) {
    return a + b;
};

ES6 Module

// import 引入
import math from './math';
// export 导出
export function sum(a,b) {
	return a + b;    
}

4 webpack的打包机制

根据import引入等关键字,将依赖文件打包成一个文件。

4-1 输出文件

输出文件的大体结构:

(function(module) {
	var installedModules = {};
    //放置已经被加载的模块
    function __webpack_require__(moduleId){
    	// SOME CODE
    }
    //
    return __webpack_require__(0); // entry file
})([ /* modules array */])

上述结构中的核心方法:

function __webpack_require__(moduleId){
	// check if module is in cache
    if(installedModules[moduleId]){
    	return installedModules[moduleId].exports;
    }
    // create a new module (and put into cache)
    var module = installedModules[moduleId] = {
    	i: moduleId,
        l: false,
        exports: {}
    };
    // exe the module func
    modules[moduleId].call{
    	module.exports,
        module,
        module.exports,
        __webpack_require__
    };
    // flag the module as loaded
    module.l = true;
    // return the exxports of the module
    return module.exports;
}

4-2 webpack打包过程

  • 从入口文件开始,分析整个应用的依赖树
  • 将每个依赖模块包装起来,放到一个数组中等待调用
  • 实现模块加载的方法,并把它放到模块执行的环境中,确保模块间可以互相调用
  • 把执行入口文件的逻辑放在一个函数表达式中,并立即执行这个函数

5 npm

理解包管理器
熟悉 npm 核心特性
理解 npm "仓库"与"依赖"的概念
理解 npm “语义化版本”
掌握使用 npm 自定义工程脚本的方法

  • package.json 属性说明
  • name - 包名
  • version - 包的版本号。
  • description - 包的描述。
  • homepage - 包的官网URL。
  • author - 包的作者,它的值是你在 npmjs.org 网站的有效账户名,遵循“账户名<邮件>”的规则,例如:zhangsan zhangsan@163.com
  • contributors - 包的其他贡献者。
  • dependencies / devDependencies - 生产 / 开发 环境依赖包列表。它们将会被安装在 node_module 目录下。
  • repository - 包代码的Repo信息,包括type和URL,type可以是git或svn,URL则是包的Repo地址。
  • main - main 字段指定了程序的主入口文件,require(‘moduleName’) 就会加载这个文件。这个字段的默认值是模块根目录下面的 index.js。
  • keywords - 关键字
  • script - 自定义命令 “仓库”

是一个遵循 npm 特定包规范的站点,提供 API 进行下载、上传、获取包信息、管理用户账号等操作。

“依赖”

在开发/生产中所需要用到的外部资源被称为依赖。

“语义化版本”

优势:方便 npm 包的发布者很便捷的把更新的小版本推送到使用者手里。

  • ^version:中版本和小版本
  • ~version:小版本
  • version:特定版本

npm install 的过程

  • 寻找包版本信息文件 (package.json),依赖这个文件进行安装
  • 查看 package.json 中的依赖,并检查项目中其他的版本信息文件
  • 如果发现新包,就更新版本信息文件
npm install packageName    //本地安装,安装到项目目录下,不在package.json中写入依赖
npm install packageName -g //全局安装,安装在Node安装目录下的node_modules下
npm install packageName --save //安装到项目目录下,并在package.json文件的dependencies中写入依赖,简写为-S
npm install packageName --save-dev 
//安装到项目目录下,并在package.json文件的devDependencies中写入依赖,简写为-D

npm install packageName 命令

  1. 安装模块到项目node_modules目录下。
  2. 不会将模块依赖写入devDependencies或dependencies 节点。
  3. 运行 npm install 初始化项目时不会下载模块。

npm install -g packageName 命令

  1. 安装模块到全局,不会在项目node_modules目录中保存模块包。
  2. 不会将模块依赖写入devDependencies或dependencies 节点。
  3. 运行 npm install 初始化项目时不会下载模块。

npm install -save packageName 命令

  1. 安装模块到项目node_modules目录下。
  2. 会将模块依赖写入dependencies 节点。
  3. 运行 npm install 初始化项目时,会将模块下载到项目目录下。
  4. 运行npm install --production或者注明NODE_ENV变量值为production时,会自动下载模块到node_modules目录中(生产环境)。

npm install -save-dev packageName 命令

  1. 安装模块到项目node_modules目录下。
  2. 会将模块依赖写入devDependencies 节点。
  3. 运行 npm install 初始化项目时,会将模块下载到项目目录下。
  4. 运行npm install --production或者注明NODE_ENV变量值为production时,不会自动下载模块到node_modules目录中(开发环境)。

总结:
所以它们的区别在 package.json 文件里面体现出来的就是,使用 --save-dev 安装的 插件,被写入到 devDependencies 域里面去,而使用 --save 安装的插件,则是被写入到 dependencies 区块里面去。

那 package.json 文件里面的 devDependencies  和 dependencies 对象有什么区别呢?

devDependencies  里面的插件只用于开发环境,不用于生产环境,而 dependencies  是需要发布到生产环境的。
比如我们写一个项目要依赖于jQuery,没有这个包的依赖运行就会报错,这时候就把这个依赖写入dependencies ;
而我们使用的一些构建工具比如glup、webpack这些只是在开发中使用的包,上线以后就和他们没关系了,所以将它写入devDependencies。

6 webpack实战

  • 实现了基本的webpack配置和实现的功能和代码配置如下: 在项目中安装和配置webpack
  • 配置打包的入口和出口
  • webpack缓存
  • webpack自动打包
  • webpack自动生成html
  • webpack打包处理css less scss
  • 配置postcss自动添加css兼容前缀
  • 打包样式表中的图片和字体文件
  • 打包处理js文件中的高级语法
  • 懒加载

我的github仓库地址 :里面有实现了上述所有功能的一个小项目,有图文链接和代码