Part2-2 模块化开发与规范标准
一、模块化演变过程
-
Stage1 文件划分方式
将每一个功能和相关的数据存放到不同的文件中,约定每一个文件都是一个独立的模块 ,通过script标签导入
<script src="module-a.js"></script> <script src="module-b.js"></script> // module a 相关状态数据和功能函数 var name = 'module-a' function method1 () { console.log(name + '#method1') } <script> // 命名冲突 method1() // 模块成员可以被修改 name = 'foo' </script>缺点:
1.污染全局作用域。
2.命名冲突问题。
3.无法管理模块依赖关系
-
Stage2 命名空间方式
所有的模块成员都挂载在一个对象上面
module-a.js // module a 相关状态数据和功能函数 var moduleA = { nameA: '', m1: function () {} } module-b.js var moduleB = { nameB: '', m2 : function () {} }缺点:
1.模块成员仍然可以修改。
2.无法管理模块依赖关系。
-
Stage3 IIFE:匿名函数自调用(闭包)
为模块提供私有空间,将模块的每一个成员都放在一个函数提供的私有作用域之中。
// module a 相关状态数据和功能函数 ;(function () { var name = 'module-a' function method1 () { console.log(name + '#method1') } function method2 () { console.log(name + '#method2') } // 将成员暴露给全局对象 window.moduleA = { method1: method1, method2: method2 } })()解决了私有作用域的问题,模块之间不能修改成员。
二、模块化规范
1.CommonJS规范:nodejs规范
- 一个文件就是一个模块。
- 每个模块都有单独的作用域。
- 通过module.exports导出成员。
- 通过require函数载入模块。
CommonJS是以同步模式加载模块,node环境下可以完美运行,在浏览器端就不行,因为会存在异步任务。
2.AMD(Asynchronous Module Definition):异步模块的定义规范
诞生了require.js,同时又是一个很强大的模块加载器。
- 每一个模块 都必须要通过define函数去定义。
- define函数默认接收两个参数
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
// 参数1:模块名,模块2:依赖项,模块3:与参数2一一对应
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
// 载入一个模块
require(['./module1'], function () {
module1.start()
})
缺点:
- 使用起来相对复杂。
- 模块js文件请求频繁。
3.ES Modules
模块化的最佳实现:浏览器环境中使用ES Modules,node环境中使用Common JS
3.1 ES Modules的特性
-
ESM 中自动使用严格模式,全局模式中的this为undefined,忽略'use strict'
<script type="module"> console.log(this); // undefined </script> <script> console.log(this); // window </script> -
每一个ES module都是一个单独的作用域
<script type="module"> const a = 1 console.log(a); // 1 </script> <script type="module"> console.log(a); // 报错 </script> -
ESM是通过CORS的方式去请求外部的js模块的
<!-- 正常访问 --> <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> <!-- Access to script at 'https://libs.baidu.com/jquery/2.0.0/jquery.min.js' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. --> <script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> -
ESM的script标签会延迟执行脚本
<!-- 先弹框,后打印 --> <script src="./01.js"></script> <p>哈哈哈哈哈哈</p> <!-- 先打印,后弹框,相当于加了defer属性 --> <script type="module" src="./01.js"></script> <p>哈哈哈哈哈哈</p>
3.2 ES Modules的核心功能
-
导出
<script type="module" src="./app.js"></script>// 01.js // export const name = 'zs' // export function start () { // console.log('start'); // } const name = 'zs' function start () { console.log('start'); } export { name, start } // export1 // 如果使用as导出成员变量,引入的时候就要引入对应的name1,start1 export { name as name1, start as start1 } // export2 // 如果导出default export { name as default, start as start1 } // export3 // export default name // export4 -
导入
// app.js import { name, start } from './01.js' // import1 // 对应as导出,导入对应的重命名之后的成员变量 import { name1, start1 } from './01.js' // import2 console.log(name1); start1() // 引入的default就要重命名,import3 import { default as name1, start1 } from './01.js' // 写法一 import name1, { start1 } from './01.js' // 写法二 // 对应as导出 console.log(name1); start1() // import name from './01.js' // import4 -
注意事项
-
导出和引入的方式是基本语法。
//导出 export { name, start } //导入 import { name, start } from './01.js' -
导出的是一个内存空间,而不是值的复制。
let name = 'zs' let age = 18 export { name, age } setTimeout(() => { name = 'ls' }, 1000) import { name, age } from './01.js' // 对应as导出 console.log(name, age); // zs 18 setTimeout(() => { console.log(name, age); // ls 18 }, 1500) -
引入的成员是一个只读成员。
import { name, age } from './01.js' name = 'wangwu' // app.js:6 Uncaught TypeError: Assignment to constant variable.报错 // 对应as导出 console.log(name, age); // zs 18 setTimeout(() => { console.log(name, age); // ls 18 }, 1500)
-
-
导入的用法
// 1.不能省略.js后缀 // import { name, age } from './01' // app.js:1 GET http://127.0.0.1:5500/part2-2/01 net::ERR_ABORTED 404 (Not Found) import { name, age } from './01.js' // 2.index.js不能省略 // import { name, age } from './utils' import { name, age } from './utils/index.js' // 3../ 不能省略 // import { name, age } from 'utils' import { name, age } from './utils/index.js' // 4.可以使用完整的路径或者url import { name, age } from 'http://127.0.0.1:5500/utils/index.js' // 5.直接导入模块使用 import './utils/index.js' // 6.使用*导出模块所有成员 import * as obj from './01.js' // Module {Symbol(Symbol.toStringTag): "Module"} // 7.动态导入 import('./01.js').then(res => { console.log(res); // Module {Symbol(Symbol.toStringTag): "Module"} }) -
ES Modules和CommonJS在node中的交互
- ES Modules中可以导入CommonJS模块
- CommonJS中不能导入ES Modules模块
- CommonJS模块始终只会导出一个默认成员
- 注意import不是结构导出对象,而是固定的语法
三、常用的模块化打包工具
1. webpack
webpack.config.js配置文件
entry
指定webpack打包文件的路径。
module.exports = {
// 当对路径时,./不能省略
entry: './src/main.js'
}
output
设置输出文件位置。
- filename:指定输出文件名称
- path:指定输出文件所在目录 ,完整的绝对路径
const path = require('path')
module.exports = {
...
// 输出文件
output: {
// 输出文件名
filename: 'bundle.js',
// 输出文件路径,完整的绝对路径
path: path.join(__dirname, 'dist'),
},
}
mode
设置打包环境,默认为production
- none 不使用任何默认优化选项
- production 使用生产配置优化选项
- development 使用开发配置优化选项
module.exports = {
// 打包环境
mode: 'none',
}
loader
文件加载器,工作原理:负责资源文件从输入到输出的转换
css-loader
样式文件加载器,需要搭配style-css使用
module.exports = {
...
module: {
rules: [
{
test: /.css$/,
// 从后往前运行
use: [
// 将css挂载到style标签内
'style-loader',
'css-loader'
]
}
]
}
}
file-loader
文件资源加载器,加载img、font ,需要配置publicPath
module.exports = {
...
// 指定文件加载目录
publicPath: 'dist/',
module: {
rules: [
{
test: /.png$/,
use: 'file-loader'
}
]
}
}
url-loader
使用Data URLs来表示文件,比如data:base64,适合小文件
module.exports = {
...
module: {
rules: [
{
test: /.png$/,
use: 'url-loader'
}
]
}
}
file-loader和url-loader的区别
- 小文件使用url-loader,减少请求次数
- 大文件单独提取存放,提高加载速度
module.exports = {
...
module: {
rules: [
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
// 超出10kb的文件单独存放,小于10kb文件转换为Data URLs嵌入代码中
limit: 10 * 1024 // 10KB
}
}
}
]
}
}
babel-loader
一般需要搭配安装babel核心模块@babel/core,具体特性转换插件集合@babel/preset-env
module.exports = {
...
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
// 指定转换插件结合,转换es6
presets: ['@babel/preset-env']
}
}
}
]
}
}
html-loader
html文件加载器
module.exports = {
...
module: {
rules: [
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
// 指定img的src属性,a链接的href属性
attrs: ['img:src', 'a:href']
}
}
}
]
}
}
Webpack模块的加载方式
- 遵循ES Modules标准的import声明
- 遵循CommonJS标准的require函数
- 遵循AMD标准的define函数和require函数
- 样式代码中的@import指令和url函数
- HTML代码中的图片标签的src属性
2. webpack开发一个loader
开发一个md加载器
// webpack.config.js
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'
]
}
]
}
}
// markdown-loader.js
const marked = require('marked')
module.exports = source => {
// console.log(source);
// return 'console.log("hello ~")'
const html = marked(source)
// 1.返回一个js代码
// return `module.exports = ${JSON.stringify(html)}`
// return `export default ${JSON.stringify(html)}`
// 2.返回一个html字符串,交给下一个loader处理
return html
}
3. webpack插件
增强webpack 自动化能力,loader负责资源加载,plugin负责自动化工作,比如清除dist目录、拷贝静态资源到输出目录、压缩输出代码
-
clean-webpack-plugin自动清理打包目录插件// yarn add clean-webpack-plugin --dev const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { ... plugins: [ new CleanWebpackPlugin() ] } -
html-webpack-plugin自动生成使用打包结果的html插件// yarn add html-webpack-plugin --dev const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { ... plugins: [ new CleanWebpackPlugin(), // 根据模板生成html,index.html new HtmlWebpackPlugin({ // 自定义输出html title: 'Webpack Plugin Sample', meta: { viewport: 'width=device-width' }, // 模板文件 template: './src/index.html' }), // 同时生成其他html,about.html new HtmlWebpackPlugin({ filename: 'about.html' }), ] } -
copy-webpack-plugin用于复制静态资源的插件// yarn add copy-webpack-plugin --dev const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist'), // publicPath: 'dist/' }, module: { ... }, plugins: [ new CleanWebpackPlugin(), // 根据模板生成html, new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', meta: { viewport: 'width=device-width' }, template: './src/index.html' }), // 拷贝public中所有文件到dist目录 new CopyWebpackPlugin([ 'public' ]) ] } -
总结
通过在生命周期的钩子中挂载函数实现扩展
// 自定义插件 class MyPlugin { apply (compiler) { console.log('myplugin qidong'); compiler.hooks.emit.tap('MyPlugin', compilation => { for (let key in compilation.assets) { // console.log(key); console.log(compilation.assets[key].source()); if (key.endsWith('.js')) { const contents = compilation.assets[key].source() const withoutComments = contents.replace(/\/\*\*+\*\//g, '') compilation.assets[key] = { source: () => withoutComments, size: () => withoutComments.length } } } }) } }
四、基于模块化工具构建现代Web应用
1.自动编译--watch
yarn webpack --watch
2.自动刷新浏览器--webpack-dev-server
yarn webpack-dev-server --open 打开浏览器
3.静态资源访问--devServer配置
module.exports = {
devServer: {
// 指定额外的静态资源路径,字符串或者数组
contentBase: './public'
}
}
4.devServer代理
注意点:If you're using webpack-cli 4 or webpack 5, change
webpack-dev-servertowebpack serve
module.exports = {
devServer: {
// 指定额外的静态资源路径,字符串或者数组
contentBase: './public',
proxy: {
'/api': {
// http://localhost:8080/api/user ---> https://api.github.com/api/users
target: 'https://api.github.com',
pathRewrite: {
'^/api': ''
},
// 不使用本机主机名
changeOrigin: true
}
}
}
}
5.source-map 源代码地图
module.exports = {
// 配置开发过程中的辅助工具
devtool: 'source-map'
}
5.1 不同类型的source-map对比:
5.2 source-map选择:
- dev环境:
cheap-module-eval-source-map- 每行代码不超过80个字符,一行可以放下,很容易定位到位置。 -- cheap
- 经过loader转换后的差异较大 。-- module
- 首次打包速度慢无所谓,重写打包速度相对较快。 -- e va l
- prod环境:
none/nosources-source-map- 会暴露我们的源代码
6.HMR: hot-module-replace 热更新
需要手动去处理模块热替换的逻辑。
// 开启热替换功能
const webpack = require('webpack')
module.exports = {
...
devServer: {
hot: true
}
...
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
// main.js
let lastM = heading
// 处理热替换
if (module.hot) {
module.hot.accept('./heading', () => {
// 自定义的内容
const val = lastM.innerHTML
document.removeChild(lastM)
const newM = new heading()
newM.innerHTML = val
document.appendChild(newM)
lastM = newM
})
}
hotOnly替换hot,暴露报错module.hot做判断- 热处理逻辑不会打包到项目里
7.DefinePlugin 为代码注入全局变量
module.exports = {
...
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: JSON.Stringify("https://XXX") // 代码片段,'"https://XXX"'
})
]
}
五、打包工具的优化技巧
1. Tree-shaking
生产模式下自动开启,去掉
dead-code
// Tree-shaking的实现
module.exports = {
optimization: {
usedExports: true, // 只导出外部使用了的成员
minimize: true // 压缩代码
},
}
注意:
Tree-shaking前提是ES Modules,babel-loader如果使用了preset-env工具函数对js进行转换(ES Modules ----> CommonJS),此时Tree-shaking 失效,最新版默认不会开启转换,所以不会失效。
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: true }] // 设置是否开启对es的转换
]
}
}
},
]
}
2. Scope-hoisting 合并模块函数
// Tree-shaking的实现
module.exports = {
optimization: {
usedExports: true, // 只导出外部使用了的成员
concatenateModules: true, // 合并模块到一个中,减少代码体积
// minimize: true, // 压缩代码
},
}
3. sideEffexts副作用
副作用:模块执行时除了导出成员之外所做的事情。 一般用于开发 npm模块时才会用到
优点:可以提供更大的压缩空间。
// index.js
export a from './a'
export b from './b' // 对于main中来说这是多余的,也就形成了sideEffexts副作用
export c from './c' // 对于main中来说这是多余的,也就形成了sideEffexts副作用
// main.js
import a from './index'
// webpack.config.js
module.exports = {
optimization: {
sideEffects: true
},
}
// package.json
{
...
// "sideEffects": false, // 确保项目中没有副作用的代码,才设置为false,没用到的就会tree-shaking掉
"sideEffects": [
"./src/a.js", // 标记哪些模块具有副作用,不会被tree-shaking掉
"*.css"
]
}
4. Code-splitting 代码分割
4.1 多入口打包
module.exports = {
entry: {
a: './a.js',
b: './b.js'
},
output: {
filename: '[name].bundle.js' // [name]占位内容
},
plugins: [
new HtmlWebpackPlugin({
title: 'a', // 标题
template: './src/a.html', // 模板
filename: 'a.html', // 文件名
chunks: ['a'] // 分别设置chunks,html只会引入对应的打包后的js文件
}),
new HtmlWebpackPlugin({
title: 'b',
template: './src/b.html',
filename: 'b.html',
chunks: ['b'] // 分别设置chunks,html只会引入对应的打包后的js文件
}),
]
}
4.2 split chunks 提取公共模块
module.exports = {
optimization: {
splitChunks: {
chunks: 'all' // 所有的公共模块会单独提取到一个文件中
}
},
}
4.3 按需加载
// 使用动态导入的方式实现按需加载,动态导入的模块会自动分包
import('/a').then({ a } => {
// 处理逻辑
...
})
import('/b').then({ b } => {
// 处理逻辑
...
})
// vue、react中可以使用动态路由懒加载的方式
4.4 魔法注释--magic comments
// import位置加入注释,相同的chunkname会打包到一起
import(/* webpackChunkName: 'a' */'/a').then({ a } => {
// 处理逻辑
...
})
import(/* webpackChunkName: 'b' */'/b').then({ b } => {
// 处理逻辑
...
})
4.5 MiniCssExtractPlugin 提取CSS到单个文件
// yarn add mini-css-exract-plugin --dev
const MiniCssExtractPlugin = require('mini-css-exract-plugin')
module.exports = {
...
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 将样式通过style标签注入
// css体积超过150k建议使用这种
MiniCssExtractPlugin.loader, // 如果使用了MiniCssExtractPlugin插件,style-loader就没用了,通过link标签注入
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
4.6 optimize-css-assets-webpack-plugin css压缩插件
// yarn add mini-css-exract-plugin --dev
// yarn add optimize-css-assets-webpack-plugin --dev
// yarn add terser-webpack-plugin --dev js压缩插件
const MiniCssExtractPlugin = require('mini-css-exract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
...
// new OptimizeCssAssetsWebpackPlugin()放到这里,只有当minimizer开启的时候采取执行,也就是生产环境的时候才会执行
optimization: {
// 自定义压缩插件,会覆盖掉原有的插件
minimizer: [
new TerserWebpackPlugin(), // js压缩
new OptimizeCssAssetsWebpackPlugin() // css压缩
]
},
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 将样式通过style标签注入
// css体积超过150k建议使用这种
MiniCssExtractPlugin.loader, // 如果使用了MiniCssExtractPlugin插件,style-loader就没用了,通过link标签注入
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin(),
// css文件压缩,所有环境都会执行这个压缩代码
// new OptimizeCssAssetsWebpackPlugin()
]
}
4.7 输出文件名 hash
[name]-[hash: 8]项目级别,只要有一个地方改动hash值就会发生变化 ,: 8指定hash长度[name]-[chunkhash: 8]同一个chunk下的才会发生变化 ,: 8指定hash长度[name]-[contenthash: 8]文件级别的hash,不同的文件不同的hash ,: 8指定hash长度
[name]-[contenthash: 8]最好的选择
// 可以缓存静态资源
const MiniCssExtractPlugin = require('mini-css-exract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
output: {
// filename: '[name]-[hash].bundle.js' // [name]-[hash] 项目级别,只要有一个地方改动hash值就会发生变化
// filename: '[name]-[chunkhash].bundle.js' // [name]-[chunkhash] 同一个chunk下的才会发生变化
filename: '[name]-[contenthash].bundle.js' // [name]-[contenthash]文件级别的hash,不同的文件不同的hash
},
...
// new OptimizeCssAssetsWebpackPlugin()放到这里,只有当minimizer开启的时候采取执行,也就是生产环境的时候才会执行
optimization: {
// 自定义压缩插件,会覆盖掉原有的插件
minimizer: [
new TerserWebpackPlugin(), // js压缩
new OptimizeCssAssetsWebpackPlugin() // css压缩
]
},
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 将样式通过style标签注入
// css体积超过150k建议使用这种
MiniCssExtractPlugin.loader, // 如果使用了MiniCssExtractPlugin插件,style-loader就没用了,通过link标签注入
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]-[hash].bundle.css'
}),
// css文件压缩,所有环境都会执行这个压缩代码
// new OptimizeCssAssetsWebpackPlugin()
]
}