在前端快速发展的今天,如果不能时刻保持学习就会很快被淘汰。分享一下前端工程化中有关于模块化开发的相关知识【以Webpack5 为主】。每天进步一点点。文章总计字数29149有点长,建议收藏,拿去吃灰也不错
污染全局作用域、命名冲突问题、无法管理模块依赖关系
这种原始方式完全依靠约定,项目上了体量就会出现各种问题
在第一阶段的基础上,将每个模块包裹成一个全局对象的方式存在
解决了命名冲突的问题,但是这种方式没有私有空间
模块中定义的成员仍然会被外部访问和修改
模块依赖关系也没有被解决
采用立即执行函数的方式
实现了私有成员的概念,确保了私有成员的安全
自执行函数的参数作为依赖声明使用
以上就是早期在没有工具和规范的情况下对模块化的落地方式
CommonJS 规范:
1、一个文件就是一个模块
2、每个模块都有单独的作用域
3、通过 module.exports 导出成员
4、通过 require 函数载入模块
5、CommonJS 是以同步模式加载模块
AMD(Asyncchronous Module Definition)
1、Require.js 实现了AMD 规范,本身也是强大的模块加载器
2、约定每个函数都要使用define去定义,使用return导出成员
3、使用 require函数 自动载入模块
4、大多数第三方库都支持AMD规范,但是:
5、AMD使用起来相对比较复杂
6、模块 JS 文件请求频繁
Sea.js + CMD 类似CommonJS,使用上和require.js 也差不多
在Node.js环境中使用 CommonJS 规范
在浏览器环境中使用 ES Modules 规范
<1> 自动采用严格模式,忽略 ‘use strict’
<2> 每个ESM模块都是单独的私有作用域
<3> ESM是通过CORS去请求外部JS模块的
<4> ESM的script 标签会延迟执行脚本
index.html:
ES Module - 模块的特性
需要显示的内容
demo.js:
alert('hello')
export 是在模块内对外暴露接口
import 是在模块内导入其他模块的接口
<1> 基础使用方法:
a.js 中导出接口:
var name = "zhangsan"
function hello () { console.log('hello') }
class Person{}
// 导出时可以使用 as 重命名
export { name as firstName, hello, Person }
// 支持默认导出
// export default name
b.js 中导入接口:
// 导入时也可以使用 as 重命名
improt { firstName, hello as helloMe, Person } from './a.js'
// 导入 a.js 中的默认导出,变量名【name】可以随便取
// import name from './a.js'
注意事项
export 导出的不是字面量对象,export {} 是一个固定的语法
import 导入的时候不是解构导出对象
export 导出的是成员的引用
export 导出的成员是只读的
<2> 在ES Modules 规范中,import的用法:
1、import 导入文件必需要有文件名称(CommonJS 可以省略文件名);import 导入文件时相对路径的 ./ 是不能省略的(会被认为是加载第三方模块,与CommonJS 相同);import 导入文件可以使用绝对路径或者是url
2、可以使用 import {} from './a.js'
或 import './a.js'
加载模块但不提取
3、可以使用 import * as obj from './a.js'
把所有的成员提取出来并放入到obj对象中
4、使用全局 import 函数动态导入成员
import('./a.js').then(function(moudle) {
console.log(moudle)
})
5、提取默认成员和具名成员
improt { name, age, default as title } from './a.js'
或者 import title, { name, age } from './a.js'
6、import 配合 export 直接导出导入的模块
export { name, default as title } from './a.js'
export { age } from './b.js'
<3> 在 Node.js 环境中 运行 ES Modules
注意事项:
① 文件名 js 需要修改为 mjs
② 在ES Modules 中可以导入 CommonJS 模块
③ CommonJS 模块中不能导入 ES Modules 模块
④ CommonJS 模块始终只会导出一个默认成员
⑤ import 不是解构导出对象,只是一个固定的用法
与 CommonJS 的差异:
ESM 中没有 CommonJS 中的下面这些模块全局成员了
require、module、exports、__filename、__dirname
Node.js 新版本进一步支持:
在 package.json 中设置"type" : "module"
, 所有文件都默认以 ES Moudles 运行,js就就不用改为 mjs,不过,这个时候如果要使用CommonJS 需要把文件后缀 js 修改为 cjs
ES Modules 存在环境兼容问题
模块文件过多,网络请求频繁
所有的前端资源都需要模块化
新特性代码需要编译
模块化JavaScript需要打包
需要支持不同类型的资源模块
前端最主流的模块打包工具有:webpack、Rollup、Parcel。
以webpack为例:
<1> 模块打包器
作为模块打包器,本身就可以解决JavaScript打包的问题,将一些零散的js代码打包到一个js文件中,其中一些兼容问题的代码可以通过模块加载器(Loader)进行编译转换。
<2> 代码拆分
可以将所有代码按照我们的需要去打包,解决了把所有文件打包到一起,文件很大的问题;可以异步加载文件,初次运行需要的资源先加载,其余的等到需要的时候再加载。
<3> 资源模块
支持以模块化的方式去载入任意类型的资源文件,例如使用JavaScript直接去import引入一个css文件。
打包工具解决的是前端整体的模块化,并不单指JavaScript模块化
目前 Webpack 已经更新到 5.x 版本,下面的一些示例主要是基于Webpack 5实现。
准备一个文件夹src,文件夹下准备两个js文件
heading.js
const heading = () => {
const element = document.createElement('h2')
element.textContent = 'Hello World'
element.addEventListener('click', () => {
alert('hello webpack')
})
return element
}
export default heading
在index.js
中 导入 heading.js
模块
import createHeading from './heading.js'
const heading = createHeading()
document.body.append(heading)
与 src 同级准备一个 html 文件,以模块化的方式引入 index.js
webpack 快速上手
此时,通过serve工具直接在终端输入 serve 开启服务【需要提前安装serve工具: npm i serve -g
】,打开浏览器后可以看到页面正常运行了,点击 Hello World 弹出 hello webpack 对话框。
webpack打包准备工作:
初始化 pack.json 文件 npm init -y
安装webpack为本地依赖 npm i webpack webpack-cli --dev
查看webpack版本确认已经安装无误 npm run webpack --version
使用webpack进行打包:
// 第一种方式,使用 `yarn webpack` 命令打包
// 第二种方式修改package.json中的scirpt
"scripts": {
"build": "webpack"
},
// 然后运行 npm run build 打包
打包后自动生成一个dist目录,目录下面是打包后的main.js文件,打包时会自动将import和export自动转换掉,所以这里不再需要 type = module
, 修改html文件
webpack 快速上手
再次使用serve工具打开服务,页面仍然正常进行
添加 webpack.config.js
文件为webpack的配置文件
如果不配置,默认打包输出到dist文件夹下的main.js
// 在Node环境下,需要遵守 CommonJS 规范
const path = require('path')
module.exports = {
entry: './src/index.js', // 指定打包入口路径,相对路径的./不能省略
output: { // output 要求是一个对象,设置输出的文件位置
filename: 'bundle.js', // 设置输出文件名
path: path.join(__dirname, 'output') // 设置输出文件路径,要求是绝对路径
}
}
Webpack 的三种工作模式:官方文档链接
'none' | 'development' | 'production'
使用命令更改打包工作模式:
npm run build --mode=production
默认打包工作模式为生产模式,自动优化打包结果
npm run build --mode=development
开发模式下,自动优化打包速度,添加一些调试需要的辅助
npm run build --mode=none
None模式下,运行最原始的打包,不会做任何额外处理
使用配置文件更改打包工作模式
// 在Node环境下,需要遵守 CommonJS 规范
const path = require('path')
module.exports = {
mode: 'none', // 修改工作模式,production/development/none
entry: './src/index.js', // 指定打包入口路径,相对路径的./不能省略
output: { // output 要求是一个对象,设置输出的文件位置
filename: 'bundle.js', // 设置输出文件名
path: path.join(__dirname, 'output') // 设置输出文件路径,要求是绝对路径
}
}
webpack 只是把所有模块放到了同一个文件当中,并提供一些基础代码,让模块与模块之间相互依赖的关系保持原有的状态。【注意打断点多调试】
webpack 默认只打包js文件,打包其他资源文件需要依赖资源模块加载器【Loader】
Loader 是 Webpack 实现整个前端模块化的核心,通过不同的 Loader 就可以加载任何类型的资源
对css文件进行打包:
首先安装依赖 npm i css-loader --dev
npm i style-loader --dev
修改 webpack.config.js
文件中的打包入口并配置模块加载规则
const path = require('path')
module.exports = {
mode: 'none', // 工作模式,production/development/none
entry: './src/main.css', // 指定打包入口路径,相对路径的./不能省略
output: { // output 要求是一个对象,设置输出的文件位置
filename: 'bundle.js', // 设置输出文件名
path: path.join(__dirname, 'output') // 设置输出文件路径,要求是绝对路径
},
module: {
rules: [ // 针对其他资源模块加载规则的配置
{
test: /.css$/, // 正则表达式匹配打包中遇到的文件路径
use: [ // 指定匹配到的文件需要使用的Loader, Loader的执行顺序是从后往前
'style-loader', // 将css-loader转换的结果通过style标签的形式追加到页面上
'css-loader' // 需要保证css代码先转换为js模块,才能正常打包
]
}
]
}
}
上面对 css 打包只是尝试一下,正确的做法还是需要把 js 作为打包入口,在js当中使用 import 的方式去引入 css 文件
比如,新增 heading.css 文件
.heading {
padding: 20px;
background: #343434;
color: #fff;
}
在 heading.js
中引入 heading.css
文件并添加类名
import './heading.css'
const heading = () => {
const element = document.createElement('h2')
element.textContent = 'Hello World'
element.classList.add('heading')
element.addEventListener('click', () => {
alert('hello webpack')
})
return element
}
export default heading
在 index.js
中导入 main.css
import createHeading from './heading.js'
import './main.css'
const heading = createHeading()
document.body.append(heading)
npm run build
重新打包,并使用serve开启服务,网页样式已修改
总结:
Webpack 建议根据代码的需要动态导入资源,因为真正需要资源的不是应用,而是代码。JavaScript 驱动了整个前端的应用,动态导入资源一是,逻辑合理,JS确实需要这些资源文件去实现相应的功能;二来可以确保上线资源不缺失,而且上线的资源文件都是必要的。
项目中遇到的图片文件和字体文件,是无法通过js的方式去表示的,就要用到文件资源加载器
首先安装依赖: npm i file-loader --dev
在 index.js
中导入图片
import createHeading from './heading.js'
import './main.css'
import icon from './icon.png'
const heading = createHeading()
document.body.append(heading)
const img = new Image()
img.src = icon
document.body.append(img)
添加文件资源模块加载规则的配置
const path = require('path')
module.exports = {
mode: 'none', // 修改工作模式,production/development/none
entry: './src/index.js', // 指定打包入口路径,相对路径的./不能省略
output: { // output 要求是一个对象,设置输出的文件位置
filename: 'bundle.js', // 设置输出文件名
path: path.join(__dirname, 'output') // 设置输出文件路径,要求是绝对路径
},
module: {
rules: [ // 针对其他资源模块加载规则的配置
{
test: /.css$/, // 正则表达式匹配打包中遇到的文件路径
use: [ // 指定匹配到的文件需要使用的Loader, Loader的执行顺序是从后往前
'style-loader', // 将css-loader转换的结果通过style标签的形式追加到页面上
'css-loader' // 需要保证css代码先转换为js模块,才能正常打包
]
},
{
test: /.png$/,
use: 'file-loader' // 文件资源加载器
}
]
}
}
总结工作过程:
Webpack 打包时遇到了图片文件,就会根据配置文件中的配置,使用文件资源加载器将导入的文件拷贝到输出的目录,再将文件拷贝到输出的目录的路径作为当前这个模块的返回值返回,这时候就可以通过模块的导出成员拿到这个资源的访问路径
Data URLs 是一种特殊的URL协议,他可以直接表示文件。表示形式如下:
协议 媒体类型和编码 文件内容
data: [][;base64],
例如图片,将文字内容进行base64编码: data: image/png;base64,iVBORw0K......lFTkSuQmCC
通过 data url就可以用代码的方式,表示任何类型的文件,需要用到专门的加载器【url-loader】
首先安装依赖: npm i url-loader --dev
将文件资源模块中文件加载规则的配置 file-loader
改为 url-loader
使用 file-loader
路径: http://localhost:5000/output/06c26fe7f9477c6d495ecb2b5331da7b.png
重新 npm run build
重新打包,然后启动服务
现在使用 url-loader
图片的路径:data:image/png;base64,iVBORw0KGgoAAAANSU ... lFTkSuQmCC
总结:
最佳的实践方式是,对于小文件使用 Data URLs,减少请求次数,大文件单独提取存放,提高加载速度。可以通过配置加载器的加载规则来实现【超出10KB的文件单独提取存放,小于10KB的文件转换为Data URLs 嵌入到代码中】。
注意:使用这种方式的前提要保证安装 file-loader 加载器
const path = require('path')
module.exports = {
mode: 'none', // 修改工作模式,production/development/none
entry: './src/index.js', // 指定打包入口路径,相对路径的./不能省略
output: { // output 要求是一个对象,设置输出的文件位置
filename: 'bundle.js', // 设置输出文件名
path: path.join(__dirname, 'output') // 设置输出文件路径,要求是绝对路径
},
module: {
rules: [ // 针对其他资源模块加载规则的配置
{
test: /.css$/, // 正则表达式匹配打包中遇到的文件路径
use: [ // 指定匹配到的文件需要使用的Loader, Loader的执行顺序是从后往前
'style-loader', // 将css-loader转换的结果通过style标签的形式追加到页面上
'css-loader' // 需要保证css代码先转换为js模块,才能正常打包
]
},
{
test: /.png$/,
// use: 'url-loader' // 文件资源加载器
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 超过10KB,使用 file-loader 加载
}
}
}
]
}
}
编译转换类:如 css-loader,将加载到的资源文件转换为JavaScript代码
文件操作类:file-loader,把加载到的资源文件拷贝到输出目录,同时将资源的访问路径向外导出
代码检查类:对加载到的资源文件代码进行校验,目的是统一代码风格,提高代码质量
因为模块打包需要,所以处理 import 和 export,它并不能转换ES6当中的其他特性。如果要转换ES6语法,需要配置一个额外的编译型loader【最常见的babel-loader】
安装babel-loader,需要同时安装 @babel/core【babel核心依赖模块】 @babel/preset-env【具体特性转换插件的集合】。npm i babel-loader @babel/core @babel/preset-env --dev
在 webpack.config.js
中指定 js 加载器为babel-loader,覆盖默认的js加载器
{
test: /.js$/,
use: { // 使用babel-loader 覆盖默认的加载器
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'] // 集合中包含了所有ES6新特性
}
}
},
1、遵循 ES Moudles 标准的 import声明
2、遵循 CommonJS 标准的 require 函数
3、遵循 AMD 标准的 define 函数和require 函数
4、Loader 加载的非 JavaScript 也会触发资源加载【例如,样式代码中的@import指令和url函数,HTML 代码中图片标签的 src 属性】
样式代码中的url函数:
// 在 main.js 文件中使用 background-url 载入图片
body {
max-width: 800px;
background-image: url(background.png);
background-size: cover;
}
样式代码中的@import指令
// 新建 reset.css 文件
* {
margin: 0;
padding: 0;
}
// 在 main.css 文件中导入 reset.css
@import url(reset.css);
body {
max-width: 800px;
background-image: url(background.png);
background-size: cover;
}
HTML 代码中图片标签的 src
新建 footer.html
文件
在 index.js
中引入 footer.html
文件
// import createHeading from './heading.js'
// import icon from './icon.png'
// const heading = createHeading()
// document.body.append(heading)
// const img = new Image()
// img.src = icon
// document.body.append(img)
import './main.css'
import footerHtml from './footer.html'
document.write(footerHtml)
配置html-loader
加载器,首先 npm i html-loader --dev
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
esModule: false // 如果图片不显示需要设置esModule为false
}
}
}
html-loader
默认只会处理图片的 src 属性,其他配置查看官网: 链接
// a 标签的 href 属性
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
esModule: false,
// attrs: ['img:src', 'a:href'] // 高版本html-loader不支持
sources: { // 新的写法
list: [
{
tag: 'img',
attribute: 'src',
type: 'src',
},
{
tag: 'a',
attribute: 'href',
type: 'src'
}
]
}
}
}
}
在项目中一般都会散落着各种各样的代码和资源文件,webpack根据配置找到其中一个文件作为打包入口,这个文件入口一般是JavaScript文件;
然后顺着入口文件当中的代码,根据代码中出现的import或者require之类的语句,解析推断出文件所依赖的资源模块;
然后分别解析每一个资源模块对应的依赖,最后就形成了整个项目中所有用到文件之间的依赖关系的依赖树;
有了个这个依赖关系树之后,webpack会遍历或者说递归这个依赖树,找到每个节点所对应的资源文件,然后根据配置文件中的rules属性,去找到这个模块所对应的加载器,然后交给我们的资源加载器去加载这个模块;
最后会将加载到的结果放入到bundle.js,从而实现整个项目的打包。
整个过程中,Loader机制是Webpack的核心,Loader实现了各种各样资源的加载,没有Loader的话,Webpack也就只能算是一个用来去打包或者合并JS模块代码的一个工具。
<1> 目标:了解 Loader 的工作原理
<2> 需求:开发 markdown-loader
<3> 要求:通过 markdown-loader ,将导入的 makdown 文件转换为 html 字符串
<4> 过程:
创建项目文件夹 gongye-loader
初始化 package.json
文件
安装 webpack webpack-cli
依赖
修改 package.json
文件中的 script
"scripts": {
"build": "webpack"
},
创建 src 文件夹,并在文件夹下新建 about.md
文件和 main.js
about.md
文件:
好好学习,天天长胖!
新建 webpack.config.js
import about from './about.md'
console.log('main.js 执行了')
与 src 目录同级,新建 markdown-loader.js
文件
并安装 解析 md 文件的模块 marked
和处理 html文件加载的Loader html-loader
markdown-loader.js
文件中:
const marked = require('marked')
module.exports = source => {
// marked 处理的结果是一个HTML的字符串
const html = marked(source)
// 将 HTML 字符串交给 html-loader 处理
return html
}
与 src 目录同级,新建 webpack.config.js
文件 并进行配置:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src.main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./makedown-loader.js'
]
}
]
}
}
npm run build
进行打包,完成后打开dist目录下的 bundle.js
文件,得到了我们想要的结果。
<5> 总结:
1、webpack 的加载资源的过程类似于一个工作管道,在这个工作过程中,可以依次使用多个loader,要求最后的输出结果必须是一段JavaScript代码;
2、Loader 内部的工作原理就是负责资源文件从输入到输出的转换。Loader其实就是一个管道的概念,此次 Loader 处理的结果可以交给下一个 Loader, 对于同一个资源可以依次使用多个Loader 完成一个功能。
插件机制是 Webpack 当中的另外一个核心特性,目的是增强 Webpack 在项目自动化方面的能力。Loader 专注负责实现资源模块的加载,从而实现项目的打包,Plugin 用来处理除了资源加载以外的自动化工作。
例如:Plugin 在打包之前清除dist目录,拷贝静态文件输出目录,压缩输出代码等。
首先安装插件,npm i clean-webpack-plugin --dev
在 webpack.config.js
中对插件进行配置
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./makedown-loader.js'
]
}
]
},
plugins: [
new CleanWebpackPlugin()
]
}
先在dist目录中新建一些不相关的文件,然后运行 npm run build
进行打包,dist目录中就只存在我们本次打包生成的文件了
<1> 自动生成HTML插件的简单使用
除了清理 dist 目录以外,还有一个需求就是自动生成使用 bundle.js
的HTML。
之前 index.html
文件都是通过硬编码写死在项目根目录下的,这样存在两个问题:一个是在项目发布的时候需要同时发布根目录下的html文件和dist目录下所有的的打包文,并且需要确定html中的路径引用都是正确的;第二个是,如果我们输出目录或者输出文件名发生变化,即打包配置发生了变化,就需要手动修改html中script标间中的引用路径。
通过 html-webpack-plugin
插件可以解决这两个问题。
首先安装插件,npm i html-webpack-plugin --dev
在 webpack.config.js
中对插件进行配置
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./makedown-loader.js'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin()
]
}
<1> 自动生成HTML插件的配置选项
一、简单的配置选项:
通过给 HtmlWebpackPlugin
这个构造函数传入一个对象参数的方式
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'webpack-gongye',
meta: {
viewport: 'width=device-width'
}
})
]
二、使用模板文件生成【大量的自定义】:
在 src 目录下新建一个 index.html
文件作为生成模板
Webpack
<%= htmlWebpackPlugin.option.title %>
在 webpack.config.js
中对指定生成HTML文件所用到模板
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'webpack-gongye',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
})
]
三、同时输出多个文件:
plugins: [
new CleanWebpackPlugin(),
// 每一个HtmlWebpackPlugin生成的对象都是一个页面
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'webpack-gongye',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
copy-webpack-plugin
拷贝静态文件:
在项目根目录下有一个public的文件夹以及文件夹下存放着 favicon.ico 文件,打包的时候需要拷贝到dist目录中
首先安装插件,npm i copy-webpack-plugin --dev
在 webpack.config.js
中对插件进行配置
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'webpack-gongye',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
new HtmlWebpackPlugin({
filename: 'about.html'
}),
new CopyWebpackPlugin({
patterns: [
{ from: 'public', to: ''}
]
})
]
总结:
clean-webpack-plugin
:自动清除输出目录文件
html-webpack-plugin
:自动生成HTML文件
copy-webpack-plugin
:拷贝静态文件
社区中提供了成百上千的插件,在有需求的时候可以提炼需求中的关键词,去GitHub上面搜索他们。每个插件的作用虽然不相同,但是用法都是相似的
相比于Loader,Plugin 拥有更宽的能力范围
插件机制的原理:通过钩子机制实现,类似于 Web 当中的事件。在 Webpack 工作的过程中,为了便于插件的扩展,几乎在每一个环节都埋下一个钩子。开发者在开发插件的时候就可以通过向这些节点上挂载不同的任务函数来扩展 Webpack 的能力。
预先定义好的钩子:参考链接
开发一个自己的插件:【清除打包过程中没有必要的注释】
// 插件必须是一个函数或者是一个包含 apply 方法的对象
class MyPlugin {
apply(compiler) { // compiler 工作过程最核心的对象,包含了所有的配置信息
compiler.hooks.emit.tap('MyPlugin', compilation => { // tap 方法注册钩子函数
// compilation => 可以理解为此次打包的上下文
for (let name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
idea1:以 HTTP Server 运行
idea2:自动编译 + 自动刷新
idea3:提供 Source Map 支持
以下从这3点增强 Webpack 开发体验 【22-31】
使用 webpack-cli
提供的 watch 工作模式,监视文件变化,自动重新打包
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
},
编译过后自动刷新浏览器:browserSync
同时使用两个工具,操作麻烦;
Webpack 不停的向磁盘写文件,browserSync
不断的读文件,效率降低
Webpack Dev Server
解决了上面的问题,它提供一个用于开发的 HTTP Server,集成了 自动编译 和 自动刷新浏览器 等功能
"scripts": {
"build": "webpack",
"watch": "webpack --watch",
"start": "webpack serve --open"
},
contentBase
:额外为开发服务器指定查找资源目录
修改配置文件,告知 dev server
,从什么位置查找文件:
devServer: {
contentBase: './public'
},
问题:开发阶段接口跨域问题
解决:在开发服务器中配置代理服务。浏览器(API请求) – > 开发服务器(代理请求) – > API 服务器
将 GitHub API 代理到开发服务器
devServer: {
contentBase: './public',
proxy: {
'/api': {
target: 'https://vue.cnfzflw.com',
pathRewrite: {
'^/api': ''
},
changeOrigin: true
}
}
},
npm run start
,直接在网页中访问:http://localhost:8080/api/index/index/banners
Source Map 解决了源代码与运行代码不一致所产生的,无法进行调试和无法对错误信息进行定位的问题
最基本的使用方式:
devtool: "source-map",
Webpack 支持很多种不同的方式,每种方式的效率和效果都各不相同
其中在Webpack5 已经不再支持 cheap-eval-source-map
和 cheap-module-eval-source-map
eval 模式:
devtool: "eval",
eval 模式构建速度快,重建速度最快,具有最佳性能的开发构建的推荐选择,不适用于生产环境
eval
模式:将模块代码放到eval函数中执行,只能定位到哪一个文件出了错误
eval-source-map
模式:也使用eval函数 执行代码,定位到文件并且定位到行和列的信息
cheap-eval-source-map
模式:只能定位到行信息,Loader处理过后的代码(webpack5 已经不支持)
cheap-module-eval-source-map
模式:只能定位到行信息,Loader处理前的代码(webpack5 已经不支持)
cheap-source-map
模式:没有用 eval 模式执行代码,Loader处理过后的代码,只能定位到行信息
inline-source-map
模式:使用dataURL的方式嵌入到代码当中,很少用
hidden-source-map.html
模式:开发工具中看不到 Source Map 的效果,但确实生成了Source Map文件
nosources-source-map
模式:可以看到错误的位置和行列信息,看不到源代码,保护源代码不被暴露
总结:
eval:是否使用 eval 函数执行模块代码
cheap- Source Map 是否包含行信息
module- 是否能够得到 Loader 处理之前的源代码
建议:
开发模式,cheap-module-eval-source-map模式,不过webpack 5 已经不支持。带有module可以得到 Loader 处理之前的源代码,方便调试,重新打包的速度也比较快
生产环境,none,不生成 Source Map ,保护源代码不被暴露;nosources-source-map 亦可
页面自动刷新后,部分页面内容会丢失
模块热更新:应用运行的过程中实时替换某个模块,应用运行状态不受影响
HMR 是 Webpack 中最强大的功能之一,也是最受欢迎的,它极大程度的提高了开发者的工作效率
HMR 集成在 webpack-dev-server
当中,webpack-dev-server --hot
即可开启 HMR ,也可以通过配置文件开启。【需要用到 HotModuleReplacementPlugin
插件】
const webpack = require('webpack')
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
样式文件修改有热更新,但是js文件有问题
Webpack 中的HMR 并不是开箱即用的,需要做一些额外的操作才可以正常工作,即需要手动通过代码去处理当模块更新过后,如何将更新的内容替换到页面中的逻辑。
疑问1:样式文件的热更新开箱即用?在 style-loader 中自动处理了样式的热更新。
疑问2:为什么样式文件可以自动处理?样式文件只需要将修改的内容覆盖到之前的样式内容即可
疑问3:为什么js文件不可以自动处理?js文件导出的内容没有规律,可能是对象,可能是字符串,也可能是函数,使用方式也不相同,没有办法实现统一的处理方法
疑问4:实际项目中没有手动处理,js照样可以热替换?使用框架开发,框架下的每种文件都是有规律的,便可以在用脚手架创建项目时内部集成 HMR 方案。例如,React 项目中要求每个文件必须导出一个函数或者类。
HMR 的 API:
为 module 提供了hot 属性,这个属性是一个对象,也是HMR 的 API的核心对象。hot 对象提供了一个 accept 方法,这个方法是用来注册当某一个模块更新过后的处理函数.
// 第一个参数:依赖模块路径;第二个参数:依赖模块更新后的处理函数
module.hot.accept('./editor', () => {
console.log('updated-editor')
})
针对 editor.js
模块 的优化
// 保存之前的 editor
let oldEditor = editor
module.hot.accept('./editor.js', () => {
// 缓存页面的内容
const innerHTML = oldEditor.innerHTML
document.body.removeChild(oldEditor)
const newEditor = createEditor()
// 将缓存的页面内容重新写入页面中
newEditor.innerHTML = innerHTML
document.body.appendChild(newEditor)
console.log('updated-editor')
})
不同的模块有不同的逻辑,处理过程也是不同的,所以 Webpack 无法提供一个通用的热替换。
module.hot.accept('./better.png', () => {
img.src = background
console('updated-img')
})
① 处理 HMR 的代码报错会导致自动刷新
devServer: {
// hot: true // 如果编译报错,会抛出错误,重新改成正确的,这个时候又会触发重新编译,整个浏览器会重新刷新
hotOnly: true // 如果编译报错,再改成正确的,重新编译,浏览器不会刷新
},
② 没启用 HMR 的情况下,HMR API 报错
因为 module.ho
t 是由 HotModuleReplacementPlugin
插件提供的,如果没开启就会报错,解决方法:
if (module.hot) { // 判断 HotModuleReplacementPlugin 是否开启
let oldEditor = editor
module.hot.accept('./editor.js', () => {
const innerHTML = oldEditor.innerHTML
document.body.removeChild(oldEditor)
const newEditor = createEditor()
newEditor.innerHTML = innerHTML
document.body.appendChild(newEditor)
console.log('updated-editor')
})
module.hot.accept('./better.png', () => {
img.src = background
console('updated-img')
})
}
生产环境和开发环境有很大的区别,生产环境注重运行效率,以更少量和更高效的代码完成业务功能;开发过程中只注重开发效率。下面是生产环境的一些优化:
① 配置文件根据环境不同导出不同配置
② 一个环境对应一个配置文件
配置文件根据环境不同导出不同配置
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = (env, argv) => {
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'source-map',
devServer: {
hot: true
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: 'file-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
if (env.production) { // webpack5 中 env 是一个对象
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
// new CopyWebpackPlugin(['public']) // webpack5 不再支持这种写法
new CopyWebpackPlugin({
patterns: [
{ from: 'public', to: '' }
]
})
]
}
return config
}
一个环境对应一个配置文件 [大型项目建议使用这种方法]
一般配置3个文件,公共的配置文件webpack.common.js
,生产环境的配置文件webpack.prod.js
,和开发环境的配置文件webpack.dev.js
在生产环境配置文件和开发环境配置文件中导入公共配置文件 const common = require('./webpack.common.js')
, 通过 webpack-merge
插件和并公共配置信息和需要修改的信息
// webpack.prod.js中:
const common = require('./webpack.common.js')
const { merge } = require('webpack-merge') // webpack5 中采用按需导出
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
devtool: false,
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{ from: 'public' }
]
})
]
})
// webpack.dev.js中:
const common = require('./webpack.common.js')
const { merge } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = merge(common, {
mode: 'development',
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin()
]
})
DefinePlugin
允许在 编译时 创建配置的全局常量,这在需要区分开发模式与生产模式进行不同的操作时,非常有用。production 环境下默认启用,向代码中注入 process.env.NODE_ENV
传递给 DefinePlugin
的每个键都是一个标识符或多个以 .
连接的标识符。
typeof
作为前缀,它会被定义为 typeof
调用。这些值将内联到代码中,从而允许通过代码压缩来删除冗余的条件判断。
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify('5fa3b9'),
BROWSER_SUPPORTS_HTML5: true,
TWO: '1+1',
'typeof window': JSON.stringify('object'),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
})
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。
Tree Shaking 不是某个配置选项,而是一组功能搭配使用后的优化效果,在 production 模式下自动开启。
其他模式如何开启:
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 压缩输出结果
// minimize: true
}
}
usedExports
负责标记枯树叶
minimize
负责摇掉它们
使用 concatenateModules
既提升了运行效率,又减少了代码的体积。这个特性又被称为 Scope Hoisting
即作用域提升。
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
minimize: true
}
最新版本的 Babel 并不会使 Tree Shaking 失效,因为在最新版本的 babel-loader 中自动关闭了 ES Module 转换的插件。
如果不确定可以在配置文件中进行配置:
presets: [
['@babel/preset-env', { modules: false }]
]
通过配置标识代码有没有副作用,为 Tree Shaking 提供更大的压缩空间,在production 环境下也会自动开启。
副作用是指模块除了导出成员是否还做了其他的事情,一般用于npm包标记是否有副作用
// webpacj.config.js 中,开启 sideEffects 特性
optimization: {
sideEffects: true
}
// package.json 中,标识所有代码都没有副作用
"sideEffects": false
在package.json
中标记哪些文件具有副作用,这样打包的时候就不会自动除去这些文件了
"sideEffects": [
"./src/extend.js",
"*.css"
]
所有代码最终打包到一起,bundle体积就会过大;
并不是所有模块在启动时都是必要的;
同域并行请求限制,每次请求都会有一定的延迟,请求的 Header 浪费带宽流量;
所以分包进行按需加载,即代码分包/代码分割就很有必要。
实现分包的两种方式:多入口打包、动态导入
一般适用于多页应用程序,一个页面对应一个打包入口,不同页面公共部分单独提取
webpacj.config.js
中进行配置
entry: { // 为每一个模块提供一个打包入口路径
index: './src/main.js',
about: './src/about.js'
},
output: {
filename: '[name].bundle.js', // 使用占位符使打包后的文件名不同
path: path.join(__dirname, 'dist'),
},
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'webpack-gongye',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html',
chunks: ['index'] // 设立一个独立的 chunk
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html',
chunks: ['about'] //设立一个独立的 chunk
}),
配置 optimization 属性 提取所有的公共模块
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'common',
chunks: 'all',
minChunks: 2,
},
},
}
},
Webpack 支持动态导入从而实现按需加载。动态导入的模块会被自动分包
实现动态导入不需要任何额外的配置,只需要通过条件判断在需要用到所需模块的时候import导入,并通过then方法进行处理加载之后需要的操作
if (hash === '#posts') {
import('./posts/posts')
.then(({ default: posts }) => { mainElement.appendChild(posts()) })
} else if (hash === '#album') {
import('./album/album')
.then(({ default: album }) => { mainElement.appendChild(album()) })
}
通过动态导入的文件的名称只是一个序号,如果需要给这些boundle
命名的话,可以使用魔法注释实现。
if (hash === '#posts') {
import(/* webpackChunkName: 'components' */'./posts/posts')
.then(({ default: posts }) => { mainElement.appendChild(posts()) })
} else if (hash === '#album') {
import(/* webpackChunkName: 'components' */'./album/album')
.then(({ default: album }) => { mainElement.appendChild(album()) })
}
如果css文件太大,比如超过150KB,可以考虑提取到单个文件;如果css文件不是太大建议还是嵌入到代码中,减少一次请求,效果可能会更好。
提取 CSS 到单个文件,实现 CSS 的按需加载:
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader,'css-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
Webpack 内置的压缩文件插件默认只针对js文件进行压缩,对css的压缩需要用 optimize-css-assets-webpack-plugin
插件。【压缩类的插件建议配置到minimizer数组中,方便统一管理,在开发环境下不会压缩文件,生产环境下会自动开启压缩】
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
optimization: {
minimizer: [
new OptimizeCssAssetsWebpackPlugin(),
new TerseWebpackPlugin()
]
},
生产模式下,文件名使用 Hash , 可以解决静态资源缓存的一些问题。
Webpack 中的 filenam属性支持通过占位符的方式为文件名设置 Hash
hash类别 | 区别 |
---|---|
普通hash | 项目级别的hash,只要项目中有文件发生变化,此次打包的所有文件hash都会变化 |
chunkhash | 模块级别的hash,同一模块的chunkhash都是相同的,文件修改后,对应模块的hash变化 |
contenthash | 文件级别的hash,文件修改后只有对应的文件hash发生变化,最适合解决缓存问题 |
指定hash长度:filename: '[name]-[hash:8].bundle.css'
只是一款 ESModule 的打包器,把散落的细小模块打包为整块代码,作用与 Webpack 非常类似,但是没有其他额外的功能,比如 Rollup 不支持类似 HMR 这种高级特性。
Rollup 更为小巧,仅仅是一款提供充分利用 ESM 各项特性的高效打包器。
初始化 package.json
文件 ;安装 rollup
在 package.json
文件 同级创建src文件夹
在scr文件夹下创建index.js
文件 logger.js
文件 messages.js
文件
// index.js 中===========
// 导入模块成员
import { log } from './logger'
import messages from './messages'
// 使用模块成员
const msg = messages.hi
log(msg)
// logger.js 中=========
export const log = msg => {
console.log('---------- INFO ----------')
console.log(msg)
console.log('--------------------------')
}
export const error = msg => {
console.error('---------- ERROR ----------')
console.error(msg)
console.error('---------------------------')
}
// messages.js 中========
export default {
hi: 'Hey Guys, I am zce~'
}
使用 rollup 打包 yarn rollup ./src/index.js --format iife --file dist/bundle.js
【输出格式为自调用函数,输出文件路径为dist目录下的bundle.js
】
打开 bundle.js
,输出结果非常简洁,只保留了引用的部分,这是因为rollup自动开启了Tree Shaking优化打包结果,Tree Shaking这个概念最早也是由rollup提出的。
创建配置文件 rollup.config.js
, 对打包进行一些配置
export default {
input: './src/index.js',
output: {
file: 'dist/bound.js', // 指定输出路径
format: 'iife' // 指定输出格式
}
}
运行 yarn rollup --config
打包
或者在 package.json
配置scripts
"scripts": {
"build": "rollup --config"
},
运行 npm run build
打包
Rollup 只是ESM的打包器,如果项目有更高级的需求,比如加载其他类型的资源文件,或者在代码中导入 CommonJS 模块,或者编译 ECMAScript 新特性,Rollup 支持 使用插件的方式实现,而且插件是 Rollup 唯一的扩展途径。
示例:使用导入JSON文件的插件
安装 插件 rollup-plugin-json
import json from 'rollup-plugin-json'
export default {
input: './src/index.js',
output: {
file: 'dist/bound.js', // 指定输出路径
format: 'iife' // 指定输出格式
},
plugins: [
json() // 导出的是一个函数,必须调用
]
}
在 index.js
中引入 package.json
// 导入模块成员
import { log } from './logger'
import messages from './messages'
import { name, version } from '../package.json'
// 使用模块成员
const msg = messages.hi
log(msg)
log(name)
log(version)
运行打包后,在 bundle.js 中 就可以看到我们想要的打包结果了
通过 rollup-plugin-node-resolve
插件,可以直接在代码中使用模块名称导入模块
安装 rollup-plugin-node-resolve
插件并配置
// rollup.config.js中的配置
import resolve from 'rollup-plugin-node-resolve'
export default {
input: './src/index.js',
output: {
file: 'dist/bound.js', // 指定输出路径
format: 'iife' // 指定输出格式
},
plugins: [
resolve()
]
}
安装lodash-es
插件,并导入使用lodash-es
模块
// 导入模块成员
import _ from 'lodash-es'
// 使用模块成员
log(_.camelCase('hello world'))
通过 rollup-plugin-commonjs
插件,可以加载 CommonJS 模块,用法和 rollup-plugin-node-resolve
插件一样。
// rollup.config.js 中的配置
import commonjs from 'rollup-plugin-commonjs'
export default {
input: './src/index.js',
output: {
file: 'dist/bound.js', // 指定输出路径
format: 'iife' // 指定输出格式
},
plugins: [
commonjs()
]
}
使用符合 ESM 标准的 动态导入的方式实现模块暗血加载,Rollup 内部会自动处理代码的拆分
注意:
1、文件输出格式不能是IIFE(自执行函数),在浏览器当中只能使用AMD标准
2、需要输出多个文件,输出目录需要使用dir设定
// index.js 中使用动态导入
import('./logger').then(({ log }) => {
log('hello')
})
// rollup.config.js 中的配置
export default {
input: './src/index.js',
output: {
dir: 'dist', // 指定输出目录
format: 'amd' // 指定输出格式
}
}
支持多入口打包,对不同入口当中的公共部分也会提取到单个的文件当中。
配置多入口打包的方式,只要将配置文件中的input属性,配置为数组,或者使用webpack当中对象的形式配置。
export default {
// input: ['src/index.js', 'src/album.js'],
input: {
foo: 'src/index.js',
bar: 'src/album.js'
},
output: {
dir: 'dist',
format: 'amd' // 多入口打包默认自动提取公共模块,自动进行代码拆分
}
}
AMD输出格式的文件不能直接引用到页面上,需要使用实现AMD标准的第三方的库去加载。
Rollup 优点:
1、输入结果更加扁平,执行效率高
2、Rollup 会自动移除未使用代码
3、Rollup 打包结果依然完全可读(和源代码一致)
Rollup 缺点:
1、加载非 ESM 的第三方模块比价复杂
2、模块最终都被打包到一个函数中,无法实现 HMR
3、浏览器环境中,代码拆分功能依赖 AMD 库
选用原则:
如果正在开发应用程序,建议选择 Webpack
如果开发 库/框架 建议使用 Rollup
PS:近几年随着 Webpack 的发展,Rollup 的优势也不存在了,比如说 Rollup 当中的扁平化输出,在 Webpack 中就可以使用 concatenateModules
将打包之后的模块合并到同一个函数中,提升运行效率,减少代码体积
完全零配置的前端应用打包器
1、Parcel 建议使用 html 文件作为打包入口
2、自动开启开发服务器,类似于 Webpack Dev Server
3、支持模块热替换
4、支持自动安装依赖
5、支持其他类型的模块加载,不需要安装其他插件
6、支持动态导入,内部自动拆分代码
7、生产环境下打包,自动开启压缩功能
安装 Parcel yarn add parcel-bundler --dev
开发环境打包:yarn parcel 文件路径
生产环境打包:yarn parcel build 文件路径
Parcel 2017年发布的首版,因为当时 Webpack 在使用上过于繁琐,而 Parcel 真正意义上做到了零配置,让开发者更专注于编码,而且构建速度更快【内部使用多进程同时工作】
目前使用更多的还是 Webpack,因为Webpack有更好的生态,出现问题更容易解决;随着发展,Webpack越来越好用