1、模块化演变过程
立即执行函数
2、commonjs规范
一个文件就是一个模块
每个模块都有点单独的作用域
通过module.exports到处
通过require导入
commonjs是以同步模式加载模块
node没问题但是浏览器段有问题
所以就要使用amd规范,require.js
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
require(['./modules/module1'], function (module1) {
module1.start()
})
使用起来较为复杂,但是生态比较好,模块划分过于细致的话js文件请求频繁
3、模块化标准规范
浏览器:ES Modules
node:CommonJS
4、ES Modules 基本特性
<!-- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 -->
<script type="module">
console.log('this is es module')
</script>
<!-- 1. ESM 自动采用严格模式,忽略 'use strict' -->
<script type="module">
console.log(this)
</script>
<!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
<!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- 4. ESM 的 script 标签会延迟执行脚本 -->
<script defer src="demo.js"></script>
<p>需要显示的内容</p>
5、ESmodule导出
var obj = { name, age }
export default { name, age }
//导出一个对象,属性为name,age
export { name, age }
// export name // 错误的用法
// export 'foo' // 同样错误的用法
setTimeout(function () {
name = 'ben'
}, 1000)
//引用方也会在一秒钟之后更改
6、import
// import { name } from 'module.js'
// import { name } from './module.js'
// import { name } from '/04-import/module.js'
// import { name } from 'http://localhost:3000/04-import/module.js'
// var modulePath = './module.js'
// import { name } from modulePath
// console.log(name)
// if (true) {
// import { name } from './module.js'
// }
动态导入,直接引用地址
// import('./module.js').then(function (module) {
// console.log(module)
// })
//命名成员和默认成员都要导入出来
// import { name, age, default as title } from './module.js'
import abc, { name, age } from './module.js'
console.log(name, age, abc)
7、
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
加上nomodule 就可以实现不支持esmodule的浏览器动态加载这个脚本
8、
ES Modules可以导入Common JS模块
CommonJs不能导入ES Modules模块
CommonJS始终导出一个默认成员
注意import不是解构导出对象
9、打包的由来
ES Modules的弊端:
模块化需要处理兼容
模块化的网络请求太频繁,因为每一个文件都要从服务端
所有前端资源除了JS之外都是需要模块化的
打包工具的功能:
编译JS新特性
生产阶段需要打包为一个js文件
支持不同种类的资源类型
10、模块打包工具
webpack:
模块打包器
兼容性处理:loader
代码拆分
资源模块:允许使用js引入其他资源
11、webpack快速上手
12、webpack运行原理
bundle这个文件是一个立即执行函数
传入参数就是代码里的每一个模块
有一个对象来储存加载过的模块,
会按照进入顺序加载模块,
13、资源模块加载
不同的资源文件需要配置不同的loader加载器
在rules里面配置
rules:[
{
test:/.css$/,
use:[
'style-loader',
‘css-loader'
]
}]
14、webpack导入资源模块
一般还是以js文件为入口
那么其他资源的文件
例如在js文件引入css文件
将整个项目变成了js驱动的项目
15、webpack文件资源加载器
安装file-loader加载器
rules:[
{
test:/.png$/,
use:[
‘file-loader',
‘css-loader'
]
}]
output:{
puclicPath:’dist/‘ 根目录文件,这样图片才能找到那个地址
}
16、URL加载器
use:{
Loader:url-loader,
options:{
Limit: 10 * 1024//低于这个大小才用url-loader
}
}]
小文件用url-loader
17、 js转换
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
18、模块加载方式
遵循ES
遵循Common
遵循AMD的define函数和require函数
Loader加载的时候也会触发
样式代码中的@import指令和url函数
@import url(reset.css);
HTML中的src属性和href属性
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
19、loader工作原理
markdown-loader
const marked = require('marked')
module.exports = source => {
// console.log(source)
// return 'console.log("hello ~")'这里是因为必须返回js语句
const html = marked(source)
// return html
// return `module.exports = "${html}"` 这样子会导致换行符等被忽略
// return `export default ${JSON.stringify(html)}` 转成json就可以避免这个问题
// 返回 html 字符串交给下一个 loader 处理
return html
}
rules: [
{
test: /.md$/,
use: [ //顺序是从后往前
'html-loader',
'./markdown-loader'
]
}
20、插件机制
增强自动化能力,例如压缩代码
loader是用来增强资源加载能力的
21、常用插件
自动清理输出目录的插件
plugins: [
new webpack.ProgressPlugin(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin()
]
根据模板动态生成,动态输出
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'
})
]
22、开发一个插件
插件比起loader有着更宽泛的能力
本质是钩子机制,webpack打包全流程中会提供钩子
class MyPlugin {
apply (compiler) {
console.log('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
}
}
}
})
}
}
23、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': '' //正则表达式替换掉api字符
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
changeOrigin: true
}
}
}
},
24、source map
调试和报错的基础,源代码的地图
//# sourceMappingURL=jquery-3.4.1.min.map
这个注释可以在浏览器当中映射出源代码
25、webpack配置
devtool: 'source-map',//这个设置可以实现source map
26、webpack eval模式的source map
devtool: 'eval',
通过eval函数执行,并且在结尾附上那段注释
构建速度是最快的
但是很难定位行列信息,只能定位到文件
27、不同devtool模式对比
module.exports = allModes.map(item => {
return {
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}
})
'eval',
'cheap-eval-source-map', //阉割版的source map,定位时定位在了源代码
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',//阉割版的source map,定位时定位在了
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
eval-是否使用eval执行模块代码
cheap-是否包含行信息
module-是否能够得到loader处理之前的源代码
inline-把sourcemap嵌入到代码当中
hidden - 没有source map的效果
nosources-能看到错误信息,但是浏览器上面看不到
如何选择呢?
选择合适的source map
开发模式中:cheap-module-eval-source-map
原因:
代码每行不超过80个字符
经过loader转换后变化较大
首次打包速度慢无所谓,重新打包较快
生产模式中:none
source map会暴露源代码
28、自动刷新
自动刷新会导致页面状态丢失
页面不刷新
29、HMR(模块热更新)
在运行过程的即时替换,应用运行状态不受影响
plugins: [
new webpack.HotModuleReplacementPlugin()
]
devServer: {
hot: true
// hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
},
// ============ 以下用于处理 HMR,与业务代码无关 ============
// main.js
if (module.hot) {先判断是否存在这个模块
let lastEditor = editor
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
// console.log(createEditor)
const value = lastEditor.innerHTML
document.body.removeChild(lastEditor)
const newEditor = createEditor()
newEditor.innerHTML = value
document.body.appendChild(newEditor)
lastEditor = newEditor
})
module.hot.accept('./better.png', () => {
img.src = background
console.log(background)
})
}
HMR的特殊逻辑会在编译之后被清掉
30、生产环境优化
mode的用法
不同环境下的配置
①、配置文件根据环境不同导出不同配置
module.exports = (env, argv) => {
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
②、一个环境对应一个配置文件
const webpack = require('webpack')
const merge = require('webpack-merge') //可以满足合并配置的功能,不会替换掉公有里面的同名属性
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
31、DefinePlugin
为代码注入
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
32、treeShaking
optimization: {
// 模块只导出被使用的成员,标记枯树枝
usedExports: true,
// 尽可能合并每一个模块到一个函数中,作用域提升
concatenateModules: true,
// 压缩输出结果,把树枝要下来
// minimize: true
}
生产环境中会自动开启
33、Tree-shaking & Babel
必须使用ES Modules
babel-loader
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 转换
// 设置为commonjs会强制转换
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true
}
34、sideEffects
optimization: {
sideEffects: true,
}
如果一个模块里被引用进来但是只用里面的一个模块,那么其它模块就不会被引入
import { Button } from './components'
//components当中除了Button之外的模块都不会被引入
要确保你的代码没有副作用
// 副作用模块
import './extend'
在extend当中为number原型添加了一个方法,这样就会导致这个方法是没有被引入的
解决办法
"sideEffects": [
"./src/extend.js",
"*.css"
]
在package.json文件中
35、代码分割
并不是每个模块都是在启动时需要的
所以需要实现按需加载
但是也不能分的太细
同域并行请求
请求头占资源
两种解决方法
①多入口打包
entry: {
index: './src/index.js',
album: './src/album.js'
},注意这里是对象,数组的话就是多个文件打包到一个文件
output: {
filename: '[name].bundle.js’//编译结束后会替换[name]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album’] //指定加载那个打包文件
})
]
提取公共模块
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
②动态导入
按需加载
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
36、minicss
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
37、OptimizeCssAssetsWebpackPlugin
optimization: {
//配置在这里可以保证只有压缩功能开启时才会执行下面插件
//所以需要自己手动保证一下js的压缩
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin() 压缩样式文件
]
},
38、输出文件名hash
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
通过hash值控制缓存
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。