模块化
- CommonJS规范:它是Node.js中所遵循的模块规范,该规范约定,一个文件就是一个模块,每个模块都有单独的作用域,通过module.exports导出成员,再通过require函数载入模块。
缺点:CommonJS约定的是以同步的方式加载模块,因为Node.js执行机制是在启动时加载模块,执行过程中只是使用模块,所以这种方式不会有问题。但是如果浏览器使用同步的加载模式,会引起大量同步模式请求,导致应用运行效率地下。
- AMD(Asynchronous Module Definition)规范:异步模块定义规范。有一个出名的库Require.js。社区提出的规范。
缺点:目前绝大多数第三方库都支持 AMD 规范,但是它使用起来相对复杂,而且当项目中模块划分过于细致时,就会出现同一个页面对 js 文件的请求次数过多的情况,从而导致效率降低。
- ES Modules:ECMAScript 2015(ES6)中定义的模块系统,逐渐被浏览器实现。
打包工具想要实现的功能
- 第一,它需要具备编译代码的能力,也就是将我们开发阶段编写的那些包含新特性的代码转换为能够兼容大多数环境的代码,解决我们所面临的环境兼容问题。
- 第二,能够将散落的模块再打包到一起,这样就解决了浏览器频繁请求模块文件的问题。这里需要注意,只是在开发阶段才需要模块化的文件划分,因为它能够帮我们更好地组织代码,到了实际运行阶段,这种划分就没有必要了。
- 第三,它需要支持不同种类的前端模块类型,也就是说可以将开发过程中涉及的样式、图片、字体等所有资源文件都作为模块使用,这样我们就拥有了一个统一的模块化方案,所有资源文件的加载都可以通过代码控制,与业务代码统一维护,更为合理。
针对上面第一、第二个设想,我们可以借助 Gulp 之类的构建系统配合一些编译工具和插件去实现,但是对于第三个可以对不同种类资源进行模块化的设想,就很难通过这种方式去解决了,所以就有了我们接下来要介绍的主题:前端模块打包工具。 目前,前端领域有一些工具能够很好的满足以上这 3 个需求,其中最为主流的就是 Webpack、Parcel 和 Rollup,我们以 Webpack 为例:
-
Webpack 作为一个模块打包工具,本身就可以解决模块化代码打包的问题,将零散的 JavaScript 代码打包到一个 JS 文件中。
-
对于有环境兼容问题的代码,Webpack 可以在打包过程中通过 Loader 机制对其实现编译转换,然后再进行打包。
-
对于不同类型的前端模块类型,Webpack 支持在 JavaScript 中以模块化的方式载入任意类型的资源文件,例如,我们可以通过 Webpack 实现在 JavaScript 中加载 CSS 文件,被加载的 CSS 文件将会通过 style 标签的方式工作。
除此之外,Webpack 还具备代码拆分的能力,它能够将应用中所有的模块按照我们的需要分块打包。这样一来,就不用担心全部代码打包到一起,产生单个文件过大,导致加载慢的问题。我们可以把应用初次加载所必需的模块打包到一起,其他的模块再单独打包,等到应用工作过程中实际需要用到某个模块,再异步加载该模块,实现增量加载,或者叫作渐进式加载,非常适合现代化的大型 Web 应用。 以上内容摘自kaiwu.lagou.com/course/cour…
默认入口
webpakc默认打包的入口是src/ 下面的文件;如果用没有设置入口,同时也没有src文件,项目打包会报错,请看截图:
loader的意义
直接对css文件编译会报错的,需要有相应的loader
还有一点需要注意的就是一旦配置多个 Loader,执行顺序是从后往前执行的,如一定要将 css-loader 放在最后,因为必须要 css-loader 先把 CSS 代码转换为 JS 模块,才可以正常打包。
Loader 是 Webpack 实现整个前端模块化的核心。因为只有通过不同的 Loader,Webpack 才可以实现任何类型资源的加载。
Loader
Webpack内部默认只能够处理JS模块代码,也就是说在打包过程中,它默认把所有遇到的文件都当作Javascript代码进行解析。其实:Webpack是用Loader(加载器)来处理每个模块的,而内部默认的Loader只能处理JS模块,如果需要加载其他类型的模块就需要配置不同的Loader。
值得注意的是:一旦配置多个Loader,执行顺序是从后往前执行的,所以这里一定要将css-loader放在最后,因为必须要css-loader先把CSS代码转换为JS模块,才可以正常打包。
自定义Loader
Webpack加载资源文件的过程类似于一个工作管道,你可以在则会个过程中依次使用多个Loader,但是最终这个管道结束过后的结果必须是一段标准的JS代码字符串。
自定义插件
整理plugin文章:segmentfault.com/a/119000001…
几个插件最常见的应用场景:
- 实现自动在打包之前清除 dist 目录(上次的打包结果);
- 自动生成应用所需要的 HTML 文件;
- 根据不同环境为代码注入类似 API 地址这种可能变化的部分;
- 拷贝不需要参与打包的资源文件到输出目录;
- 压缩 Webpack 打包完成后输出的文件;
- 自动发布打包结果到服务器实现自动部署。
Webpack 要求我们的插件必须是一个函数或者是一个包含 apply 方法的对象,一般我们都会定义一个类型,在这个类型中定义 apply 方法。然后在使用时,再通过这个类型来创建一个实例对象去使用这个插件。
打包原理
- Webpack CLI 启动打包流程;
- 载入 Webpack 核心模块,创建 Compiler 对象;
- 使用 Compiler 对象开始编译整个项目;
- 从入口文件开始,解析模块依赖,形成依赖关系树;
- 递归依赖树,将每个模块交给对应的 Loader 处理;
- 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。
工具
webpack-dev-server
webpack-dev-server 是 Webpack 官方推出的一款开发工具,根据它的名字我们就应该知道,它提供了一个开发服务器,并且将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成在了一起。
运行 webpack-dev-server 这个命令时,它内部会启动一个 HTTP Server,为打包的结果提供静态文件服务,并且自动使用 Webpack 打包我们的应用,然后监听源代码的变化,一旦文件发生变化,它会立即重新打包,大致流程如下:
配置选项
Webpack 配置对象中可以有一个叫作 devServer 的属性.
- 静态资源访问
- Proxy 代理
//webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/main.js',
output: {
filename: "bundle.js"
},
devServer: {
contentBase: 'public',
compress: true,
port: 39001,
proxy: {
'/api': {
target: 'https://api.github.com',
pathRewrite: {
'^/api': '' // 替换掉代理地址中的 /api
},
changeOrigin: true // 确保请求 GitHub 的主机名就是:api.github.com
}
}
},
plugins: [
new HtmlWebpackPlugin()
]
}
热替换(HMR)
HMR全称Hot Module Replacement, 模块热替换 或 模块热更新
- 自己写的JS或者CSS代码要想实现热替换是需要额外的代码的。
import './style/main.css'
import createEditor from './edit';
let divContent = document.createElement('div');
divContent.innerText = 'Hello, webpack, 123'
document.body.appendChild(divContent);
const editor = createEditor();
document.body.appendChild(editor);
console.log('test');
let lastEditor = editor;
module.hot.accept('./edit', ()=>{
console.log('update...')
// 当 editor.js 更新,自动执行此函数
// 临时记录更新前编辑器内容
const value = lastEditor.value;
debugger;
//移除更新前的元素
document.body.removeChild(lastEditor);
// 创建新的编辑器
// 此时 createEditor 已经是更新过后的函数了
lastEditor = createEditor()
// 还原编辑器内容
lastEditor.value = value
// 追加到页面
document.body.appendChild(lastEditor)
})
- 在使用框架开发的时候,我们项目中的每个文件都有规律。vue-cli 或者 create-react-app 这种框架脚手架工具内部都已经实现了通用的替换操作,自然不需要手动处理了。
优化
Source Map
Source Map(源代码地图)映射转换后的代码与源代码直接的关系。一段转换后的代码,通过转换过程中生成的Source Map文件可以逆向解析得到对应的源代码。
开发环境一般选择:cheap-module-eval-source-map:
- 因为这种名字中带有 module 的模式,解析出来的源代码是没有经过 Loader 加工的。
- 阉割版的 eval-source-map,因为它虽然也生成了 Source Map 文件,但是这种模式下的 Source Map 只能定位到行,而定位不到列,所以在效果上差了一点点,但是构建速度会提升很多。
生产环境选择none, 不生成Source Map。
// ./webpack.config.js
module.exports = {
devtool: 'eval'
}
tree-shaking
生产打包webpack时开启了tree-shaking的。也可以自己配置:
// ./webpack.config.js
module.exports = {
optimization: {
//模块只导出被使用的成员
usedExports: true,
//压缩输出结果
minimize: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true
}
}
这就是 Tree-shaking 的实现,整个过程用到了 Webpack 的两个优化功能:
- usedExports - 打包结果中只导出外部用到的成员;
- minimize - 压缩打包结果。
- concatenateModules - 尽可能合并每一个模块到一个函数中,尽量缩小减少打包后文件体积。
如果把我们的代码看成一棵大树,那你可以这样理解:
- usedExports 的作用就是标记树上哪些是枯树枝、枯树叶;
- minimize 的作用就是负责把枯树枝、枯树叶摇下来。
** Tree-shaking 实现的前提是 ES Modules,也就是说:最终交给 Webpack 打包的代码,必须是使用 ES Modules 的方式来组织的模块化。**
很多时候,我们为了更好的兼容性,会选择使用 babel-loader 去转换我们源代码中的一些 ECMAScript 的新特性。而 Babel 在转换 JS 代码时,很有可能处理掉我们代码中的 ES Modules 部分,把它们转换成 CommonJS 的方式,如下图所示:
所以可能babel-loader 会失效。
sideEffects
在components/index.js里面统一导入导出。在main.js中只使用其中某个函数。如果没有使用sideEffects,只是进行tree-shaking,那么打包后还是有些代码是 冗余的:
这些console毕竟不是export方法什么的,所以tree-shaking并不能删除。
这个时候使用 sideEffects 可以去掉 这些没有使用的模块中的冗余的代码。
这里设置了两个地方:
- webpack.config.js 中的 sideEffects 用来开启这个功能;
- package.json 中的 sideEffects 用来标识我们的代码没有副作用。