前言
模块化思想
何为模块化思想?
模块化思想其实就是将一块代码按照特定的功能进行拆分,每个代码段有各自的目的,可以对它进行独立的设计,开发,测试,最终通过接口将它们组合在一起。
在传统的使用script标签的方式引入到页面中,这种做法的弊端是:
- 需要手动维护JavaScript的加载顺序。当这些
script存在依赖关系的时候,需要手动的调整顺序,防止因为依赖问题导致出现问题。 - 多个
script,请求也多,意味着每一个script标签都请求一次静态资源,过多的请求会拖慢网页的性能。 - 每个
script的顶层作用域都是全局作用域,如果没有做处理,将会导致全局作用域污染问题。
模块化将解决这些问题:
- 通过导入导出语句,我们就知道模块间的依赖关系
- 借助打包工具,我们可以将一些资源进行合并,压缩,减少网络开销
- 多个模块的作用域是隔离的,不会出现命名冲突问题
模块化思想有:AMD、CommonJS、CMD,ES Module等
模块化的变化过程
使用函数进行封装
我们可以将不同逻辑和功能封装到不同的全局函数中,这样逻辑可以解耦
写法:
// 基数
const base = 10// 加法
function add(a, b) {
return a + b + base
}
// 减法
function subtract(a, b) {
return a - b - base
}
// 乘法
function multiply(a, b) {
return a*b*base
}
上面就是最简单的例子,分别实现了加减乘方法,并且加入了一个基数,以后扩展和复用就可以自己进行组合了。
缺点: 会造成全局变量污染,容易引发命名冲突,并且关系不明确
使用命名空间的形式
命名空间的写法我们一般使用a.b.c.d,一层一层的找对应的值,使用我们可以使用简单的对象进行封装我们的逻辑,这样就可以减少全局变量,从而减少变量污染,命名冲突的问题。
写法:
let convertFun = {
base: 10,
add: function(a, b) {
return a + b + this.base
},
subtract: function(a, b) {
return a - b - this.base
},
multiply: function(a, b) {
return a*b* this.base
}
}
上面我们可以看出,我们将之前的四个换算方法用一个对象包裹起来了,相当于多了一层命名空间,访问的话就需要使用convertFun.add(1,2)来进行访问,这样就可以与全局变量进行隔离开来。
缺点: 内部的方法和状态会被外部随意改动,比如外部随意就修改了base
自执行函数
从上面来看,我们只需要解决私有化变量的问题就能算一个好的模块的了。
为了不影响到全局作用域,那么我们可以使用自执行函数来做隔离,因为它在编译的时候就执行了,执行完就释放,不影响其他全局变量, 它就相当于一个window一样,那么我们如何实现像定义全局变量一样,在里面定义变量呢?答案就是利用闭包,函数内部能访问到外部的变量,那么闭包则不会立即回收。
用法:
(function(window) {
let base = 10
function add(a, b) {
return a + b + base
}
// 减法
function subtract(a, b) {
return a - b - base
}
// 乘法
function multiply(a, b) {
return a*b*base
}
// 挂载
window.convertFun = {
add,
subtract,
multiply
}
})(window)
这样外界就不能修改到base了,相当于base变成了一个私有属性。 看到上面的写法,不禁想到了jQuery,也是使用这种方式实现的,源码如下:
(function(window) {
var jQuery = (function() {
// 逻辑
return jQuery
})();
// .....
window.jQuery = window.$ = jQuery
})(window)
但是这种写法还有一个问题,那就是依赖问题,当这个模块依赖另一个模块的时候,那么就需要新增传参解决,那么就可以使用下面的形式将依赖传进入。
(function(window, jQuery) {
let base = 10
function add(a, b) {
// jQuery.....
return a + b + base
}
// 减法
function subtract(a, b) {
// jQuery.....
return a - b - base
}
// 乘法
function multiply(a, b) {
// jQuery.....
return a*b*base
}
// 挂载
window.convertFun = {
add,
subtract,
multiply
}
})(window, jQuery)
这些就是模块化的进化过程,但是当我们有多个这样的script文件被引入到一个里面的时候,就会导致请求过多,依赖不清晰,加载顺序问题。 为了解决这些问题,我们还需要借助一些模块化思想,也就是模块化规范。
为什么选择Webpack
- 默认支持多种模块标准,包括AMD,CommonJS,以及ES6模块。
- Webpack有完备的代码分割解决方案。首屏只加载必要的部分,不太重要的功能放到最后动态加载,有效提高首屏加载速度。
- Webpack处理各种类型的资源。
- 拥有庞大的社区支持。除了核心库以外,还有很多周边的插件和工具。
安装和启动
安装
首先想用webpack打包的话,必须要安装webpack。
我们先可以使用npm init -y 快速初始化一个项目
然后使用npm install webpack@4.29.4 --save-dev,这里我们安装的是4.29.4版本的,每个版本都可能不一样,建议安装相同版本,除了用npm外,还可以用yarn add来进行安装,这里就不多说了。那么安装分为全局安装和本地安装,我们使用的是本地安装save-dev,不影响你的全局版本的。
除了安装webpack外,还需安装webpack命令行工具webpack-cl,所以我们使用npm install webpack-cli@3.2.3,同样也是要注意版本一致。
如果安装成功,我们可以使用npx webpack -v和npx webpack-cli -v看是否安装成功。
如下是全部过程:
// 初始化项目
npm init -y
// 安装webpack核心模块
npm install webpack@4.29.4 --save-dev
// 安装webpack命令行工具
npm install webpack-cli@3.2.3 --save-dev
// 验证是否安装成功
npx webpack -v
npx webpack-cli -v
启动
下面我们来打包第一个应用。
-
先初始化文件目录:
首先我们先新建一个test1的文件夹,表示测试1,后续都是单独新建文件夹来做测试的。
然后再test1中新建一个index.html文件和一个src文件夹,在src中新增一个index.js,然后再新增一个utils文件夹,里面新增一个add.js的文件
-
写入代码:
我们在test1/src/utils/add.js中写一个add方法
export default function(a, b) { return a + b }然后在外面的test1/src/index.js中写上
import add from "./utils/add"; document.write('问题: 1+2等于几? 1+2='+ add(1, 2))最后我们在test1/index.html模板中引入一个假设我们打包好的bundle.js,它默认会打包进根目录的dist,所以我们会将bundle.js放到dist中的test1下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="../dist/test1/bundle.js"></script> </body> </html> -
执行打包命令:
打包命令以webpack开头,有很多配置,我们暂时只需要关心最基础的输入输出配置即可,也就是--entry和--output
npx webpack --entry=./test1/src/index.js --output-filename=./test1/bundle.js --mode=development
执行命令会得到如下命令输出:
按照上面的步骤来,最后能在根目录中看见多出了个dist,并且在dist中有个test1,里面就有bundle.js,也就是我们打包后的文件。
我们一般工作中,会使用npm run build代替这个很长的命令,那么我们可以在package.json中的scripts对象中加上这个
"scripts": {
"build": "webpack --entry=./test1/src/index.js --output-filename=./test1/bundle.js --mode=development"
}
最后打包出来也是一样的
我们还可以简化,只执行webpack命令,当执行webpack命令的时候,将会去找webpack.config.js配置文件,我们一般将这个文件放在根目录下,将命令行的配置方式改成了key-value的形式。
// packages.json
"scripts": {
"build": "webpack" // 简化后
}
// webpack.config.js
var path = require('path')
module.exports = {
entry: './test1/src/index.js',
output: {
path: path.join(__dirname , '/dist'),// 要求使用绝对路径
filename: './test1/bundle.js',
},
mode: 'development'
}
这样我们就算是启动了webpack打包的能力了,这些是最最最基础的入门了,后续将介绍更多打包方法。
如果我们想在本地开发方便一点的话,我们可以使用便捷的本地开发工具webpack-dev-server,也就是本地起一个服务,当保存的时候,重新打包,然后生效(模块热替更)。它仅仅在开发环境下使用,所以可以使用以下命令进行安装,
npm install webpack-dev-server --save-dev
在生产环境中,我们不需要它,因此放在了devDependencies中比较恰当。如果我们在工程上线的时候进行安装依赖,那么我们可以使用npm install --production 进行过滤掉devDependencies中的冗余模块,从而快速安装和发布。
下面我们来使用一下这个工具:
我们先在package.json中添加dev命令,让我们在运行npm run dev的时候就启动。
// package.json
{
"scripts": {
"build": "webpack"
"dev": "webpack-dev-server"
}
}
然后我们要在webpack.config.js中进行配置devServer:
// webpack.config.js
var path = require('path')
const htmlPlugin = require('html-webpack-plugin')
module.exports = {
entry: './test1/src/index.js',
output: {
path: path.join(__dirname , '/dist'),// 要求使用绝对路径
filename: './test1/bundle.js',
},
plugins: [new htmlPlugin({ title: '测试'})],
mode: 'development',
devServer: {
publicPath: '/',
port: 3000
}
}
除此之外,我们还需要npm install html-webpack-plugin --save-dev , 它的作用自动在dist文件夹下生成index.html,并自动引入打包后的js。这样访问localhost:3000,就可以访问到打包后的内容。
webpack-dev-server可以看作一个服务者,也就是服务器,它的主要工作就是接收浏览器的请求,然后将资源返回。当执行npm run dev启动服务的时候,然后进行模块打包并将资源打包好也就是bundle.js,当它接收到请求时,它会对URL地址校验。如果地址是配置的资源服务地址publicPath,就会将webpack的打包结果中将资源返回给浏览器。如果URL的地址不属于资源服务地址,则直接读取硬盘中的源文件返回。
所以webpack-dev-server有两个作用:
- 进行模块打包,并处理打包后结果的资源请求
- web服务,处理静态资源请求并返回给客户端
所以有些教程publicPath设置为/dist, 如果我们访问localhost:3000, 就会出现如下情况:
因为URL和publicPath的资源地址不匹配,所以返回硬盘的资源,如果我们想看到打包的结果,我们就可以访问localhost:3000/dist即可,同时我们可以设置publicPath为/,这样可以直接访问localhost:3000,最后的访问结果如下:
需要注意的是,用Webpack开发和使用webpack-dev-server开发有区别,Webpack开发打包每次都生成新的bundle.js,而webpack-dev-server只是将打包结果放在内存中,并不会生成实际的bundle.js,每次接收到请求时将内存中的打包结果返回给浏览器,所以我们删除dist目录,如果不存在,仍然可以正常访问。这也是因为在开发的过程中,我们会经常改动目录和文件名,如果每次都将这些改动打包进去dist,就会产生很多垃圾文件。并且它能够自动刷新live-reloading,能提升本地开发效率。
到此为止,我们就已经熟悉了webpack的安装和打包,后续我们将学更多的打包配置和原理。