模块化开发
模块化开发时当前最重要的前端开发范式之一 模块化只是思想
一、模块化演变过程
Stage1 文件划分方式
- 污染全局作用域
- 命名冲突问题
- 无法管理模块依赖
- 早起模块化完全依靠约定
Stage2 命名空间方式
- 每个模块只暴露一个全局对象,所有模块都挂载到这个对象上
- 减少了命名冲突的可能
- 但是没有私有空间,模块成员可以在外部被访问或修改
- 模块之间的依赖关系没有得到解决
Stage3 IIFE 立即执行函数
- 使用立即执行函数包裹代码,要输出的遍历挂载到一个全局对象上
- 变量拥有了私有空间,只有通过闭包修改和访问变量
- 参数作为依赖声明去使用,使得每个模块的依赖关系变得明显
二、模块化规范
1. commonjs规范:
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.export导出成员
- 通过require函数载入模块
- commonjs以同步的方式加载模块
2.AMD(异步的模块定义规范)
- 模块加载器:Require.js
// 定义一个模块 define
// ['jquery', './module2'] 依赖项
// function() {} 是依赖项目的导出成员
// module1模块名
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
// 载入一个模块 require
require(['./module1'], function (module1) {
module1.start()
})
- 目前绝大多数第三方库都支持AMD规范
- AMD使用起来相对复杂
- 模块JS文件请求频繁
3. sea.js + CMD (通用模块定义规范)
定义:
// CMD 规范 (类似 CommonJS 规范)
define(function (require, exports, module) {
// 通过 require 引入依赖
var $ = require('jquery')
// 通过 exports 或者 module.exports 对外暴露成员
module.exports = function () {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
4. ES Modules(es6)模块化标准规范
浏览器(chrome, safari, 火狐。。。):遵循ES Modules(es6)
nodejs : 遵循 CommonJS
ES Modules(es6): 目前前端最主流的模块化方案
5. ES Modules 特性
// 1.通过给script添加type=module的属性,九剋哦以es module 的执行标准执行其中的js代码
<script type="module">
console.log('this is a module')
</script>
// 2.es module 自动采用严格模式
<script type="module">
console.log(this)
</script>
// 3.es module 自动采用严格模式
<script type="module">
console.log(this) //
</script>
// 4.每个es module 都是运行在单独的私有作用域中
<script type="module">
var foo=100
console.log(foo) //
</script>
<script type="module">
console.log(foo) // 报错 foo is not defined
</script>
// 5.es module 是通过cors的方式请求外部js模块的
<script type="module" src="https://unpkg.com/jquery@3.4.1/jquery.min.js"></script>
// 6.es modulede script标签会延迟执行脚本 等同于defer
<script type="module" src="demo.js"></script>
<p>需要现实的内容</p>
- 自动采用严格模式,忽略’use strict’
- 每个ESM模块都是单独的私有作用域
- ESM是通过CORS去请求外部JS模块的
- ESM的script标签会延迟脚本执行
6.ES Modules 导出
http-server
browser-sync
browser-sync . --file **/*.js
// ./modulejs
// 私有作用域
const foo = 'es modules'
class Person{}
function hello() {}
export { foo, hello, Person }
// 默认导出
export default var name ='liz'
// 使用as 重新命名
export { foo as fooName}
//使用
// import { fooName } from './module.js'
// export var foo = 'es modules'
// export function hello(){}
// export class Person{}
// 使用
// import {foo} from './module.js'
// ./app.js
import { foo } from './module.js'
console.log(foo) // => es modules
7.ES Modules 导入导出的注意事项
- export {} 是一个固定的用法,并不是字面量对象。
- export default {name, age} // 这个{}表示:导出对象字面量。导出的是这个成员的引用地址,并不是复制这个值。
- import {name, age} 这个{}表示:固定用法
- 导出的是一个只读的,并不能修改。
8.ES Modules 导入用法
import
- 文件目录名称:必须是一个完整的路径
- 相对路径中不能省略
./
- 可以是绝对路径,可以是url地址
- 执行某个文件,并不提取。 import “./moule” import {} from ‘’./module’
*
导入所有的成员 import * as mod from ‘./module.js’- 不能嵌套在if条件语句中
- 动态导入模块
- 返回的是一个promise import ("./module.js").then(()=>{})
- export 和export default 同时使用
module.js
export {name, age}
export default 'default export '
app.js
import {name, age, default as title } from './module.js'
// 方法2
import title, {name, age} from './module.js' // ,左边表示:默认导出模块
9.ES Modules 直接导出导入成员
export {foo, bar } from './module.js'
所有的导入成员会作为导出成员
使用场景:
// button.js
export var button = ' button'
// tag.js
export var tag = ' tag'
// index.js
import {button} from './button.js'
import {button} from './tag.js'
export {button, button}
// 方法2:
export {button} from './button.js'
export {button} from './tag.js'
10.ES Modules 浏览器环境 Polyfill
-
browser-es-module-loader
-
cdn地址: https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/
两个文件地址都需要引入 。 -
ie如果不支持promise,需要再引入promise-Polyfill cdn地址:https://unpkg.com/browse/promise-polyfill@8.2.0/dist/
-
<script nomodule> </script>
如果浏览器支持promise,但是也写了cdn地址,则会加载两次,可以使用nomodule这个属性,使浏览器只加载一次。
11. ES Modules in Node.js - 支持情况
- 直接在nodejs中使用es module
index.mjs
import {foo, bar} from './module.mjs
console.log(foo, bar)
// 系统内置成员
import fs from 'fs'
fs.writeFileSync('./foo.js', 'es module working')
// import {writeFileSync} from './fs'
fs.writeFileSync('./bar.js', 'es module working') // 写入成功。因为:系统模块兼容了es module 的提取成员方式。
// 载入第3方
import _ from 'lodash'
console.log(_.camelCase('ES module')) // esModule
//不能使用这种方式载入第3方模块。不支持,因为第3方模块都是导出默认成员
import {camelCase} from 'lodash'
console.log(_.camelCase('ES module')) // 报错
module.mjs
export var foo="foo"
export var bar="bar"
执行命令: node --experimental-modules index.mjs
12. ES Modules in Node.js - 与 CommonJS 交互
es-module.js
import mod from './commonjs.js '
// 1.es module 中可以导出CommonJS模块
console.log(mod) // {foo :'this is a foo'}
// 2. 不能直接提取成员,注意import 不是解构导出对象
import {foo} from './commonjs.js'
console.log(foo)
// export const foo ='es module export value'
commonjs.js
// 1.CommonJS模块始终只会导出一个默认成员
module.export = {
foo :'this is a foo'
}
exports.foo = "commonjs exports value"
// 2。不能在CommonJS模块中通过require载入es module模块
const mod = require('./es-module.js')
console.log(mod) // 报错
- ES Modules中可以导入CommonJS模块
- CommonJS模块不能导入ES Modules模块
- CommonJS始终只会导出一个默认成员
- 注意import 不是解构导出对象,而是固定用法。
13. ES Modules in Node.js - 与 CommonJS 的差异
commonjs.js
// 加载模块函数
console.log(require)
// 模块对象
console.log(module)
// 导出对象别名
console.log(exports)
// 当前文件的绝对路径
console.log(__filename)
// 当前文件所在目录
console.log(__dirname)
es.mjs
console.log(import)
14. ES Modules in Node.js - 新版本进一步支持
模块化标准规范
- 在node.js中使用CommonJS
CommonJS是node.js内置的模块化工具,只需要遵循CommonJS的标准即可,不需要引入别的依赖 - 在浏览器中使用ES Modules
ES Modules是ECMAScript2015
三、常用的模块化打包工具
模块化打包工具的由来:ES Modules存在环境兼容问题、模块文件过多,网络请求频繁,而且所有的前端资源都需要模块化。
打包工具解决的是前端整体的模块化,并不单指JavaScript模块化
- webpack
webpack:模块打包器、模块加载器、代码拆分、载入资源模块
(1) webpack的基本使用
- 先在项目的根目录下执行
yarn init -y,
创建package.json - 安装webpack相关依赖:
yarn add webpack webpack-cli --dev
- 查看webpack版本:
yarn webpack --version
, 4.43.0 - 执行yarn webpack进行打包,生成了dist目录,里面有main.js文件
- 修改
index.html
中的index.js
的路径为dist/main.js
,并且去掉script
标签的type=module
的属性 - 去package.json的scripts中定义一个build任务:
"build": "webpack",
以后执行yarn build进行打包.
(2) webpack的配置文件
webpack.config.js
文件是运行在nodejs文件下的js文件,我们需要按照CommonJS的方式编写代码。这个文件需要导出一个对象,我们完成对应的配置选项。
webpack.config.js
文件内容:
const path = require('path')
module.exports = {
entry: './src/index.js', // 指定打包入口文件,如果是相对路径,前面的点不能少
output: {
filename: 'bundle.js', // 输出文件的名称
path: path.join(__dirname, 'output'), // 输出路径,为绝对路径
}
}
(3) webpack工作模式
直接执行webpack打包的时候,控制台会有警告:
WARNING in configuration
The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.
You can also set it to ‘none’ to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
说没有指定工作模式,默认以生产模式打包,会进行代码的压缩。
我们可以通过cli命令指定工作模式,就是增加一个--mode
的参数,属性有三种选择,production、development、none
- production:生产模式会默认启动优化,优化我们的打包结果
- development:开发模式,会自动优化打包的速度,添加一些调试过程中的辅助到代码中
- none:原始状态的打包,不会做任何处理
可以通过yarn webpack --mode development
来执行.
此外,还可以在webpack的配置文件中指定工作模式,也就是增加一个mode属性,例如:mode: "development"
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
},
module: {
rules: [
]
}
}
(4) webpack资源模块加载
将配置文件中的entry
属性的值改为./src/main.css,
然后执行打包命令yarn webpack
,会报错:
ERROR in ./src/main.css 1:5
Module parse failed: Unexpected token (1:5)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
body {
| margin: 0 auto;
| padding: 0 20px;
error Command failed with exit code 2.
因为webpack默认会把文件当做js解析,所以打包css文件时,文件内容不符合JS语法则报错了,报错中提示我们可以寻找正确的loader去解析代码。webpack内部的loader只能解析js,所以我们要手动安装css-loader去处理css代码。
执行命令:yarn add css-loader --dev
然后在webpack的配置文件中增加属性:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.css$/,
use: [
'css-loader'
]
},
]
}
}
我们增加外部的loader需要在配置文件中增加资源模块module属性,属性值是一个对象,对象中有一个rules数组,数组里每个元素都是一个对象,对象中的test属性是正则式,指明要处理的资源文件,use属性是对该资源进行处理的loader名称。
再次执行打包命令,发现css没有作用,是因为我们使用css-loader只是对css文件进行了打包,但是并没有作用到页面上,接下来还要安装一个style-loader
,执行命令:yarn add style-loader --dev
style-loader
是将css-loader
处理后的结果,通过style的形式追加到页面上
然后将配置文件中的rules
对应的处理css资源模块的use
属性由'css-loader'
改为['style-loader', 'css-loader']
,use配置了多个loader,是一个数组,里面的loader是从右往左
执行,所以要将css-loader
写在后面,我们要先用css-loader
将css
代码转化成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: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
]
}
}
(5) webpack 导入资源模块
虽然webpack的入口文件可以是别的类型文件,但由于前端项目是由JS驱动,所以我们开发时一般将入口文件设置为JS文件,需要用到CSS时,就直接在JS文件中通过import导入即可,如:import './main.css'
webpack建议我们根据代码的需要在JS中动态导入资源文件,因为需要资源的不是应用,而是代码。因为是JavaScript驱动了整个前端应用,这样做的好处是:
- 逻辑合理,JS确实需要这些资源文件
- 确保上线资源不缺失,都是必要的
(6) webpack文件资源加载器
安装文件资源加载器:yarn add file-loader --dev
,相当于直接拷贝物理文件。不过此时资源文件路径会出现问题,webpack
默认认为它打包过后的文件会放在网站的根目录下面,此时需要在配置文件中的output
属性中指定publicPath
属性值为dist/
,即:publicPath: 'dist/'
,这样在打包时,文件的输出路径前面会拼接上publicPath
的值。dist/
中的/
不能省略。
文件加载器的工作过程:
webpack在打包时,遇到了图片文件,根据我们配置中的配置匹配到对应的文件加载器,此时文件加载器就开始工作,先将导入的文件拷贝到输出目录,然后再将文件拷贝到输出目录过后的路径作为当前模块的返回值返回,那么对我们应用来说,我们需要的资源就被发布出来了。我们也可以根据模块的导出成员拿到资源的访问路径。
webpack.config.js
const path = require('path')
module.exports = {
output: {
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.png$/,
use: 'file-loader'
}
]
}
}
(7) webpack URL 加载器
格式:协议 + 媒体类型和编码 + 文件内容
格式: data:[][;base64],
例如:data:text/html;charset=UTF-8,<h1>html content</h1>
-
先安装url-loader:yarn add url-loader --dev
-
修改png文件的loader为url-loader
{
test: /.png$/,
// use: 'file-loader',
use: 'url-loader'
}
- 执行yarn webpack,此时的png文件的URL则为data协议的了。
最佳使用方式:
- 小文件使用Data URLs,减少请求次数
- 大文件独立提取存放,提高加载速度
{
test: /.png$/,
// use: 'file-loader',
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024, // 单位是字节 10KB
}
}
}
- 超过10KB的文件单独提取存放
- 小于10KB文件转换为Data URLs嵌入代码中
注意:这种方式还是要安装file-loader,因为对超出大小的文件还是会调用file-loader,如果没有file-loader会报错。
(8) webpack 常用加载器分类
-
编译转换类型,转换为JS代码,如css-loader,
-
文件操作类型,将资源文件拷贝到输出目录,将文件访问路径向外导出,如:file-loader
-
代码检查器,对代码校验,统一代码风格,提高代码质量,如:es-loader
(9) webpack 处理ES2015
因为模块打包需要,所以可以处理import和export,除此之外,并不能转换其他的ES6特性。如果想要处理ES6,需要安装转化ES6的编译型loader,最常用的就是babel-loader,babel-loader依赖于babel的核心模块,@babel/core和@babel/preset-env
@babel/preset-env --dev具体转换器
- 执行命令:
yarn add babel-loader @babel/core @babel/preset-env --dev
-修改js的loader
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'] // 包含了全部的es6最新特性
}
},
exclude: /(node_modules)/, // 这里很重要,千万别忘了,否则会出错的。
}
- webpack只是打包工具
- 加载器可以用来编译转换代码
(10) webpack的模块加载方式
- 遵循ES Modules标准的import声明
// 1. 支持 ES Modules 的 import 声明
import createHeading from './heading.js'
import better from './better.png'
import './main.css'
const heading = createHeading()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
- 遵循CommonJS标准的require函数。对于ES的默认导出,要通过require(’./XXX’).default的形式获取
// 2. 支持 CommonJS 的 require 函数
const createHeading = require('./heading.js').default
const better = require('./better.png')
require('./main.css')
const heading = createHeading()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
- 遵循AMD标准的define函数和require函数
// 3. 支持 AMD 的 require / define 函数
define(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
const heading = createHeading.default()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
})
require(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
const heading = createHeading.default()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
})
-
Loader加载的非JavaScript也会触发资源加载
- 样式代码中的@import指令和url函数
main.js
import './main.css' import footerHtml from './footer.html' document.write(footerHtml)
reset.css
*{ margin:0px; padding:0px; }
main.css
@import url(reset.css); body { margin: 0 auto; padding: 0 20px; max-width: 800px; background: url(1.png); background-size: cover; }
footer.js
<footer> <!-- <img src="better.png" alt="better" width="256"> --> <a href="better.png">download png</a> </footer>
css-loader在处理css代码时,遇到了background属性中的url函数,发现是引入的资源文件是png格式的文件,则将这个资源文件 交给url-loader处理
- HTML代码中的图片标签的src属性
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
// html-loader默认只处理页面中的img标签的src属性的资源文件,所以指定其他标签的资源文件也要处理
attributes: {
list: [
{
tag: 'img',
attribute: 'src',
type: 'src'
},
{
tag: 'a',
attribute: 'href',
type: 'src'
}
]
}
}
}
}
(11) webpack的核心工作原理
在我们的项目中会散落着各种资源文件,那么webpack会根据我们的配置找到.js文件为入口文件,一般是js文件,然后顺着入口文件中的代码,代码中出现的import 和require解析推断这个文件所依赖的资源模块,分别解析每个资源模块对应的依赖,最后得到一个依赖关系树,然后递归这个依赖关系树,找到每个节点所对应的资源文件,最后根据配置的rules属性去找到这个模块所对应的加载器,交给这个加载器去加载模块,将加载到的结果放到bundle.js,从而实现整个项目的打包。整个过程中loader机制是webpack的核心,如果没有loader没有办法实现各种资源的加载,那么webpack只能算是一个打包合并代码的工具了。
loader机制是webpack的核心。
(12) webpack Loader的工作原理
实现一个markdown的文件加载器。
Loader
作为webpack
的核心机制,内部的工作原理也非常简单,我们通过开发一个自己的loader
,通过这个过程,来深入了解loader
的工作原理。
我们的需求是实现一个markdown
文件的加载器,这个加载器可以在代码当中直接导入markdown
文件。markdown
文件一般是被转换为html
过后再去呈现到页面上的,所以我们导入的markdown
文件得到的结果就是转换过后的html
字符串。
在项目的根目录下新建一个markdown-loader.js
文件,每一个webpack
的loader
都需要去导出一个函数,这个函数就是我们这个loader
的对我们所加载到的资源的一个处理过程,它的输入就是我们资源文件的内容,输出就是我们此次加工过后的一个结果。
那我们通过source
参数去接收输入,然后通过我们的返回值去输出,那这里呢,我们先尝试打印一下这个source
,然后直接去返回一个字符串hello
,我们去看一下结果,我们回到webpack配置文件中去添加一个加载器的规则配置,那这里呢,我们匹配到的扩展名就是.md
,就是我们刚刚所编写的markdown-loader
的模块,那这里呢,我们的use
属性不仅仅只可以使用模块的名称,其实对于模块的文件路径也是可以的,那这一点呢其实与node
当中的require
函数是一样的,所以说我们大家直接使用相对路径去找到这个markdown-loader
的,那配置好过后呢,我们运行打包命令,打包过程当中命令他们确实打印出来了我们所导入的markdown
的内容,那这也就意味着我们的source
确实是所导入的文件内容,但是呢,它同时也爆出一个解析错误,那说的是you many need additional load to handle the result of this loader,
就是我们还需要一个额外的加载器来去处理我们当前的加载结果,那这究竟是为什么呢?
module.exports = source => {
console.log(source)
return 'hello'
}
其实webpack的加载资源的过程有点类似于一个工作管道
,你可以在这个过程当中一次去使用多个loader
,但是呢,还要求我们最终这个管道工作过后的结果必须是一段JavaScript
代码,那因为我们这返回的内容是一个hello
,那它不是一个标准的JavaScript
代码,所以我们这才会出现这样的错误提示,那知道这个错误的原因过后呢,解决的办法其实也就很明显了,那要么就是我们这个loader
的直接去返回一段标准的JavaScript
代码,要么就是我们再去找一个合适的加载器,接着去处理我们这里返回的结果。
那这里呢,我们先来尝试第1种办法:回到我们markdown-loader.js
的当中,那这里我们将返回的这个内容修改为'console.log("hello")'
,那这就是一段标准的JavaScript
代码,然后呢,我们再一次运行打包,那此时打包过程当中就不再会报错了。
module.exports = source => {
console.log(source)
return 'console.log("hello")'
}
那这里呢,我们一起来看一下打包过后的结果究竟是什么样的,我们打开bundle.js
当中,然后呢,我们找到最后一个模块,那这里呢其实也非常简单,webpack
打包的时候就是把我们刚刚loader
加载过后的结果也就是返回的那个字符串直接拼接到我们这个模块当中了,那这也就解释了刚刚为什么说loader
的管道最后必须要去返回JavaScript
代码的原因,因为如果说你随便去返回一个内容的话,那放到这里语法就有可能不通过。
那知道了这些过后,我们再回到markdown-loader.js
的当中,然后接着去完成我们刚刚的需求,那这里呢,我们先去安装一个markdown
解析的模块叫做marked
,安装命令为:yarn add marked --dev
安装完成过后,我们再回到代码当中去导入这个模块。然后在我们的加载器当中去使用这个模块,去解析来自参数当中的这个source
,我们的返回值就是一段html
字符串,也就是转换过后的结果,那这儿如果说我们直接去返回这个html
的话,那就会面临刚刚同样的问题,那正确的做法呢,就是把这段html
变成一段JavaScript
代码,那这里呢,其实我们希望是把这一段html
作为我们当前这个模块导出的字符串,也就是我们希望通过export
导出这样一个字符串,但是如果说我们只是简单的拼接的话,那我们html当中存在的换行符还有它内部的一些引号,拼接到一起就有可能造成语法上的错误,所以说这里我使用一个小技巧,就是通过JSON.stringify
先将这个字符串转换为一个标准的JavaScript
格式字符串,那此时内部的引号以及换方符都会被转义过来,然后我们再参与拼接,那这样的话就不会有问题了,我们再次运行打包,看一下打包的结果,那此时我们所看到的结果就是我们所需要的了,当然了,除了module.exports
这种方式以外,webpack
的还允许我们在返回的代码当中直接去使用ESModule
的方式去导出.
const marked = require('marked')
module.exports = source => {
// console.log(source)
// return 'console.log("hello")'
const html = marked(source)
console.log(html)
// 两种导出方式:
// return `module.exports=${JSON.stringify(html)}`
return `export default ${JSON.stringify(html)}`
}
通过第1
种方式解决了我们刚刚所看到的那样一个错误,我们再来尝试一下刚刚所说的第2
种方法,那就是在我们markdown-loader
的当中去返回一个html
的字符串,然后我们交给下一个loader
处理这个html的字符串,我们直接去返回marked的解析过后的html,然后我们再去安装一个用于去处理html加载的loader,叫做html-loader
,完成过后,我们回到配置文件当中,那这里呢,我们把use
属性修改为一个数组,那这样的话我们的loader
工作过程当中就会依次去使用多个loader
,那不过这里需要注意,就是它的执行顺序是从数组的后面往前面,那也就是说我们应该把先执行的loader放到后面,后执行的loader放到前面。
const marked = require('marked')
module.exports = source => {
// console.log(source)
// return 'console.log("hello")'
const html = marked(source)
console.log(html)
return html
}
module: {
rules: [
{
test: /.md$/,
use: ['html-loader', './markdown-loader.js']
}
]
}
回到命令行进行打包,此时我们打包的结果仍然是可以的,我们marked
处理完的结果是一个html的字符串,然后这个html字符串交给了下一个loader,也就是html-loader
,那这个loader
又把它转换成了一个导出这个字符串的一个JavaScript
代码,那这样的话我们webpack
再去打包的时候就可以正常工作了。
那通过以上的这些尝试我们就发现了loader
它内部的一个工作原理其实非常简单,就是一个从输入到输出之间的一个转换,那除此之外,我们还了解了loader
,它实际上是一种管道的概念,我们可以将我们此次的这个loader
的结果交给下一个loader
去处理,然后我们通过多个loader
去完成一个功能,那例如我们之前所使用的css-loader
和style-loader
之间的一个配合,包括我们后面还会使用到的,像sass或者less这种loader他们也需要去配合我们刚才所说道的这两种loader
,这就是我们loader
的工作管道这样一个特性。
完整代码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Webpack - 开发一个 Loader</title>
</head>
<body>
<script src="dist/bundle.js"></script>
</body>
</html>
src/main.js
import about from './about.md'
console.log(about)
src/about.md
# 关于我
我是汪磊,一个手艺人~
markdown-loader.js
const marked = require('marked')
// source是所导入的文件内容
module.exports = source => {
console.log(source)
// return 'console.log("hello ~")'
const html = marked(source)
// return html
// return `module.exports = "${html}"`
// return `export default ${JSON.stringify(html)}`
// 返回 html 字符串交给下一个 loader 处理
return html
}
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', // 模块名称
'./markdown-loader' // 模块路径
]
}
]
}
}
package.json
{
"name": "11-markdown-loader",
"version": "0.1.0",
"main": "index.js",
"author": "zce <w@zce.me> (https://zce.me)",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"html-loader": "^0.5.5",
"marked": "^0.7.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9"
}
}
(13) Webpack插件机制
插件机制的是webpack
一个核心特性,目的是为了增强webpack
自动化方面的能力。
Loader
专注实现资源模块的加载,从而去实现整体项目的打包。
Plugin
解决除了资源加载以外的其他的一些自动化工作:
- 自动在打包之前去清除dist目录
- 拷贝那些不需要参与打包的资源文件到输出目录(拷贝静态文件至输出目录)
- 压缩我们打包结果输出的代码(压缩输出代码)
- 自动在打包之前去清除dist目录
- 安装:yarn add clean-webpack-plugin --dev
- webpack.config.js:
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin()
]
- .自动生成HTML插件
- 安装:yarn add html-webpack-plugin --dev
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin()
]
- 自动生成HTML文件到dist目录中,根目录下的index.html则不再需要了
- HTML中自动注入了bundle.js的引用到HTML文件中
- 增加配置参数生成HTML文件:
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
}
})
- 通过模板文件生成HTML文件, webpack.config.js中指定HtmlWebpackPlugin的template参数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack</title>
</head>
<body>
<div class="container">
<h1><%= htmlWebpackPlugin.options.title %></h1>
</div>
</body>
</html>
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
})
]
- .生成多个HTML页面
plugins: [
new CleanWebpackPlugin(),
// 用于生成index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成about.html
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
- 拷贝那些不需要参与打包的资源文件到输出目录
- 安装:yarn add copy-webpack-plugin --dev
const CopyWebpackPlugin = require('copy-webpack-plugin')
new CopyWebpackPlugin({
patterns: ['public']
})
- 压缩我们打包结果输出的代码
(14) webpack开发插件
相比于Loader,Plugin拥有更宽的能力范围,Plugin通过钩子机制实现。
Webpack要求插件必须是一个函数或者是一个包含apply方法的对象。通过在生命周期的钩子中挂载函数实现扩展。
webpack.config.js
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 启动')
// MyPlugin插件名称
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation 可以理解为此次打包的上下文
for (const name in compilation.assets) {
// console.log(name) // 文件名
console.log(compilation.assets[name].source())
if(name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
使用:
plugins: [
new MyPlugin()
]
(15) webpack开发体验问题
- 自动进行编译:执行
yarn webpack --watch
会监视文件的变化自动进行打包 - 自动刷新浏览器:
webpack-dev-server
,安装:yarn add webpack-dev-server --dev
,执行:yarn webpack-dev-server --open
(16) webpack dev server 静态资源访问
Dev Server默认只会serve打包输出文件,只要是webpack打包输出的文件都会被访问到,其他静态资源也需要被server
devServer: {
contentBase: './public'
}
contentBase额外为开发服务器指定查找资源目录
(17) webpack Dev server代理API
webpack-dev-server支持配置代理:
devServer: {
contentBase: './public',
proxy: {
'/api': {// 以/api开头的地址都会被代理到接口当中
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用localhost:8080作为请求GitHub的主机名
changeOrigin: true, // 以实际代理的主机名去请求
}
}
}
- 代理请求路径前缀: /api
- 代理目标: target:‘https://api.githun.com’
- 代理路径的重写: pathRewrite
- 如果请求的是http://localhost/api/uses相当于请求的是https://api.githun.com/users
(18) Source Map
运行代码与源代码之间完全不同,如果需要调试应用,错误信息无法定位,调试和报错都是基于运行代码,SourceMap 就是解决这种问题的最好办法。
Source Map解决了源代码与运行代码不一致所产生的问题.
Webpack 支持sourceMap 12种不同的方式,每种方式的效率和效果各不相同。效果最好的速度最慢,速度最快的效果最差
eval函数可以运行字符串当中的js代码:eval(console.log(123))
当DevTool的值为eval,打包后的报错信息只有源代码文件名称,没有行列信息
每个关键词的特点组合:
- eval- 是否使用eval执行代码模块
- cheap- Source map是否包含行信息
- module-是否能够得到Loader处理之前的源代码
- inline- SourceMap 不是物理文件,而是以URL形式嵌入到代码中
- hidden- 看不到SourceMap文件,但确实是生成了该文件
- nosources- 没有源代码,但是有行列信息。为了在生产模式下保护源代码不被暴露
开发模式推荐使用:eval-cheap-module-source-map
,因为:
- 代码每行不会太长,没有列也没问题
- 代码经过Loader转换后的差异较大
- 首次打包速度慢无所谓,重新打包相对较快
生产模式推荐使用:none
,原因
- Source Map会暴露源代码
- 调试是开发阶段的事情
- 对代码实在没有信心可以使用
nosources-source-map
(19) webpack HMR
HMR(Hot Module Replacement)
模块热替换,应用运行过程中,实时替换某个模块,应用运行状态不受影响。
webpack-dev-serve原理
:从内存中读取文件的变化,减少了磁盘读写的操作。提升效率。
同源部署,没有必要去开启代理服务。
webpack-dev-server自动刷新导致的页面状态丢失。我们希望在页面不刷新的前提下,模块也可以即使更新。热替换(HMR)只将修改的模块实时替换至应用中。
HMR
是webpack
中最强大的功能之一,极大程度的提高了开发者的工作效率。
HMR
已经集成在了webpack-dev-server
中,运行webpack-dev-server --hot
,也可以通过配置文件开启.
// 手动配置hmr这些配置处理css模块热替换,
// 1.
devServer: {
hot: true
// hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
},
// 2.
const webpack = require('webpack')
new webpack.HotModuleReplacementPlugin()
运行 yarn webpack-dev-server --open
// 3.处理js模块热替换
if (module.hot) {
module.hot.accept('依赖模块路径',()=>{
console.log('模块hmr')
} )
}
Webpack
中的HMR
并不是对所有文件开箱即用,样式文件支持热更新,脚本文件需要手动处理模块热替换逻辑。而通过脚手架创建的项目内部都集成了HMR
方案。
webpack中的hmr需要手动处理模块热替换逻辑
- 为什么样式文件的热更新开箱即用?
样式文件是经过loader
处理的,在style-loader
中就已经自动处理了样式文件的热更新,所以就不要我们自己额外做手动的操作。 - 凭什么样式文件可以自动处理,而脚本文件就不会自动处理?
在样式文件模块更新过后,它只需要把更新过后的css及时替换到页面当中就可以覆盖之前的样式,从而实现样式文件的更新,而我们所编写的js模块是没意义任何规律的,在一个模块当中可能导出的是一个对象,也有可能导出的是一个字符串,还有可能导出的是一个函数,对导出的成员我们的使用各不相同的,所以webpack面对这些毫无规律的js模块,根本就不知道如何去处理更新过后的文件,因此就没有办法实现通用所有情况的模块替换方案。 - 我的项目没有手动处理,js照样可以热替换?
你使用了摸个框架,框架下的开发,每种文件都是有规律的。通过脚手架创建的项目内部都继承了hmr方案。
总结:我们需要手动处理js模块更新后的热替换。
HMR注意事项:
- 处理HMR的代码报错会导致自动刷新
- 没启动HMR的情况下,HMR API报错
- 代码中多了很多与业务无关的代码
Webpack 使用 HMR API:
if (module.hot) {
module.hot.accept('依赖模块路径',()=>{
console.log('模块hmr')
} )
}
js模块热替换没有一个通用的模式。
处理HMR的代码报错会导致自动刷新。解决:开启:hotOnly:true
devServer: {
hot: true
hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
},
(20) webpack生产环境优化
我们在生产环境中,更注重开发效率,而在生产环境中,更注重开发效率。
模式(mode)
webpack建议我们为不同的环境创建不同的配置,两种方案:
- 配置文件根据环境不同导出不同配置
const path = require('path')
const webpack = require('webpack')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, argv) => {
const config = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /.md$/,
use: ['html-loader', './markdown-loader.js']
}
]
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
// 开发过程最好不要使用这个插件
// new CopyWebpackPlugin({
// patterns: ['public']
// }),
// new MyPlugin(),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
contentBase: './public',
proxy: {
'/api': {// 以/api开头的地址都会被代理到接口当中
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用localhost:8080作为请求GitHub的主机名
changeOrigin: true, // 以实际代理的主机名去请求
}
},
// hot: true
hotOnly: true, // 如果热替换代码报错了,则不刷新
},
devtool: 'eval-cheap-module-source-map'
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: ['public']
})
]
}
return config
}
- 一个环境对应一个配置文件
Webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: `bundle.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `index.html`
})
]
}
Webpack.dev.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
module.export = merge(common, {
mode: 'development',
})
Webpack.prod.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: ['public']
})
]
})
Package.json
"scripts": {
"build": "webpack --config webpack.prod.js"
},
执行打包命令:yarn build
(21) webpack优化配置
- DefinePlugin 为代码注入全局成员,这个内置插件默认就会启动,往每个代码中注入一个全局变量process.env.NODE_ENV
const webpack = require('webpack')
plugins: [
new HtmlWebpackPlugin({
filename: `index.html`
}),
new webpack.DefinePlugin({
API_BASE_URL: JSON.stringify('http://api.example.com')
})
]
- Tree-Shaking 摇掉代码中未引用到的代码(dead-code),这个功能在生产模式下自动被开启。Tree-Shaking并不是webpack中的某一个配置选项,而是一组功能搭配使用后的效果。
modulex.exports ={
optimization: {
usedExports: true, // 只导出外部使用的成员 。 usedExports:负责标记[枯树叶]
minimize: true // 开启代码压缩功能。 minimize负责【摇掉】 他们
}
}
- 合并模块函数 concatenateModules, 又被成为Scope Hoisting,作用域提升
优化输出。尽可能将所有的模块合并输出到同一个函数中。即提升了运行效率,又减少了代码的体积。
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true
}
- 很多资料中说如果使用Babel-Loader,会导致Tree-Shaking失效,因为Tree-Shaking前提是ES Modules,由Webpack打包的代码必须使用ESM,为了转化ES中的新特性,会使用babel处理新特性,就有可能将ESM转化CommonJS,而我们使用的@babel/preset-env这个插件集合就会转化ESM为CommonJS,所以Tree-Shaking会不生效。但是在最新版babel-loader关闭了转换ESM的插件,所以使用babel-loader不会导致Tree-Shaking失效。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
- sideEffects 副作用,指的是模块执行时除了导出成员之外所做的事情,sideEffects一般用于npm包标记是否有副作用。如果没有副作用,则没有用到的模块则不会被打包。在production模式默认开启。在使用时确保代码没有副作用,否则用了就会误删代码。
在webpack.config.js中开启这个功能:
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true,
sideEffects: true
}
在package.json里面增加一个属性sideEffects,值为false,表示没有副作用,没有用到的代码则不进行打包。确保你的代码真的没有副作用,否则在webpack打包时就会误删掉有副作用的代码,比如说在原型上添加方法,则是副作用代码;还有CSS代码也属于副作用代码。
"sideEffects": false
也可以忽略掉有副作用的代码:修改package.json
"sizeEffects": [
"./src/extend.js",
"*.css"
]
(22) Code Splitting 代码分包/代码分割
webpack的一个弊端:所有的代码都会被打包到一起,如果应用复杂,bundle会非常大。而并不是每个模块在启动时都是必要的,所以需要分包、按需加载。物极必反,资源太大了不行,太碎了也不行。太大了会影响加载速度;太碎了会导致请求次数过多,因为在目前主流的HTTP1.1有很多缺陷,如同域并行请求限制、每次请求都会有一定的延迟,请求的Header浪费带宽流量。所以模块打包时有必要的。
目前的webpack分包方式有两种:
- 多入口打包:适用于多页应用程序,一个页面对应一个打包入口,公共部分单独抽取。
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
// 每个打包入口形成一个独立的chunk
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Nulti Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album'] //指定需要注入的bundle.js
})
],
// 不同的打包入口肯定会有公共模块,我们需要提取公共模块:
optimization: {
splitChunks: {
chunks: 'all'
}
}
- 动态导入:需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包。通过动态导入生成的文件只是一个序号,可以使用魔法注释指定分包产生bundle的名称。相同的chunk名会被打包到一起。更灵活。
// webpackChunkName: webpack分包打包的名字
import(/* webpackChunkName: 'posts' */'./post/posts').then({default: posts}) => {
mainElement.appendChild(posts())
}
(23) MiniCssExtractPlugin可以提取CSS到单个文件:实现csss的按需加载
当css代码超过150kb左右才建议使用。
const MiniCssExtracPlugin = require('mini-css-extract-plugin')
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', //将样式通过style标签注入
MiniCssExtracPlugin.loader,
'css-loader'
]
}
]
},
(24) OptimizeCssAssetsWebpackPlugin 压缩输出的CSS文件
webpack仅支持对js的压缩,其他文件的压缩需要使用插件。
可以使用 optimize-css-assets-webpack-plugin
压缩CSS
代码。放到minimizer
中,在生产模式下就会自动压缩
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
// 这个插件并没有配置在plugins中,而是配置在了optimization优化中。因为在plugins中什么时候都可以打包,配置在optimization中,只有在optimization特性开启时才会工作。
optimization: {
minimizer: [
new TerseWebpackPlugin(), // 指定了minimizer说明要自定义压缩器,所以要把JS的压缩器指指明,否则无法压缩。手动开启js压缩。
new OptimizeCssAssetWebpackPlugin()
]
}
(25)输出文件名hash
生产模式下,文件名使用Hash。
- 项目级别的hash
output: {
filename: '[name]-[hash].bundle.js'
},
- chunk级别的hash
output: {
filename: '[name]-[chunkhash].bundle.js'
},
- 文件级别的hash,:8是指定hash长度 (推荐)
output: {
filename: '[name]-[contenthash:8].bundle.js'
},