T1:模块化开发
模块化的演变 #2
早期的模块化
没有相关工具帮助
文件划分方式 => 命名空间方式 => IIFE
解决了 污染全局作用域、命名冲突问题、无法管理模块依赖关系 这三个问题。
模块化规范出现
Node.js 下有 CommonJS 规范,即:
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过 module.exports 导出成员
- 通过 require 函数载入模块
但是 CommonJS 是同步的,不适合浏览器端。
适应浏览器端,出现了AMD (Asynchronous Module Definition) + Require.js,它提供了 define() 来定义模块、require() 来添加依赖。
也出现了 CMD (Common Module Definition) + Sea.js,它采用了与 CommonJS 相似的规范,便于上手。
当前的模块化规范
CommonJS in Node.js
ES Module in Browsers
ES Module 的使用
特性 #5
在 HTML 中使用 <script type="module"> 引入模块,内容中的 js 可以正常执行。
-
ESM 自动采用严格模式,忽略
use strict。严格模式下,全局的 this 不指向全局对象 window,而是 undefined
-
每个 module 都是运行在单独的私有作用域中
-
ESM 是通过 CORS 去请求外部 JS 模块的。
跨域请求外部模块时,需要标头注明源地址,且服务端同意。
-
ESM 的 script 标签会延迟执行脚本
即默认 differ,在网页渲染完成后执行脚本
导出 export #6
模块内的变量、函数、类都可以导出,使用 export 有两种方式:
- 直接写在导出对象的声明前,如
export var name = 'Tom' - 【常用】(在模块尾)将各项以
{}包裹导出,如export { name, foo }
导出时可以设置默认项和重命名,如 export { name as newName, foo as default }
重命名的项在被导入时,需要以新名字导入,即 import newName;
默认项需在被导入时重命名,如 import default as newFoo,或简写为 import newFoo
【注意】
- 多项导出时,并非导出了一个对象,而是导出的固定语法。同样的,导入时,也非解构对象。
- 导入的项获取到的是引用地址,而非该项的一份新拷贝。也即,导出项的变化,导入端可以实时获取到,但它是只读的,不能在导入端修改。
导入 import #8
-
导入时,from 后跟具体的模块文件URL
- 模块文件的扩展名不能省略
- URL 可以是相对 URL,以
./开头,也可以是绝对 URL,以/开头,还可以是完整的 URL 地址,例如 CDN 上的模块文件。
-
导入的另一种用法:
import {} form './module.js'或简写为import './module.js',执行该模块,而不提取其中导出。 -
全部导入某一模块的导出:
import * as mod。mod 则是包含所有导出项的对象 -
动态导入模块使用函数
import(),如import('./module.js').then(function (mod) {console.log(mod)})。模块内的导出通过 then 得到。 -
同时导入默认和命名成员:
import {name, age, default as newFoo} form './module.js'或import newFoo, {name, age} from './module.js'
同时导入和导出 #9
export {name, age} from './module.js'
这一写法通常用在使用一个模块管理多个模块导入导出的情况。将多个模块的导出导入到 index.js,并同时导出,这样在主页只需导入 index.js 模块即可。
ES Module 的兼容性处理 #10
对于不支持 ES Module 的浏览器如何处理呢?
-
引入
babel-browser-build和browser-es-module-loader,转化为通用语法。可以使用 script 标签从 unpkg.com 引入。
-
也不支持 Promise 的浏览器还需引入
promise-polyfill。 -
为以上引入添加
nomodule属性,避免正常浏览器执行两次代码。
上述的引入方法请求过多,不适用于上线运行。
ES Module in Node.js
Node.js 也逐步加入了对 ES Module 的支持
与浏览器下的主要区别:模块扩展名为 .mjs(同时注意修改导入文件的 URL)
ES 模块与 CJS 模块的交互 #11~12
ESM 导入 CJS 模块的方法:
CJS 下,模块的导出是以对象形式,包含了各种属性、方法。因此在 ES 模块下只能默认引入,引入的是 module.exports 对象,使用库时需要调用对象再调用属性方法。
e.g. import _ from 'lodash'; console.log(_.camelCase(Import From CJS Module))
没有对 ESM 做兼容的第三方包便必须以此形式引人。也有第三方包是分离出来一个专用的 ES 包,例如 lodash-es
e.g. import camelCase from 'lodash-es'; console.log(camelCase(Import From ES Module))
Node.js 的内置模块做了兼容,在模块内做了两种导出,因此上述两种导入方式均可。
Node.js 下,CJS 模块不能导入 ES 模块。
ES 模块与 CJS 模块的差异 #13
ESM 中无法获取到 CommonJS 中的全局成员了,包括 module、require()、exports、__filename、__dirname
require 和 exports 实现引入和导出,在 ESM 中有 import 和 export 对应负责。
__filename 和 __dirname 使用以下方法替代:
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
Node.js 对 ESM 的新支持 #14
可在 package.json 中加入 'type': 'module' 字段,那么 js 文件即以 ESM 运行了,而 CJS 模块则需要修改扩展名为 .cjs 才可正常运行。
旧版 Node 中使用 ESM(借助 babel)#15
安装 @babel/node ,配置转码规则为 @babel/plugin-transform-modules-commonjs 或包含它的 @babel/preset-env
即可在命令行中 babel-node esModule.js 直接调用 ES 模块。
T2:Webpack 打包工具
基本使用方法
将多个 js 模块打包为一个 js 文件,此时在 HTML 的引入无需 type="module" 属性
配置文件 #4
在根目录创建 webpack.config.js 文件
- 入口
- 输出
const path = require('path')
module.exports = {
entry: './src/app.js', //入口,默认是'./src/main.js'
output: {
filename: 'bundle.js', //文件名,默认是'main.js'
path: path.join(__dirname, 'output')//输出绝对路径
}
}
-
工作模式 #5
e.g.
mode: 'none'不同的工作模式相当于不同的配置组,也可添加命令行参数
--mode XXX使用。默认 production 模式。- production 上线用
- development 优化打包速度,添加开发用说明
- none 无附加
Webpack 的加载器 loader
Webpack 默认 loader 可以打包 js 模块,其他资源模块需要新增 loader 来处理。
loader 是 Webpack 的核心 #14。
常用 loader 分类 #11
-
编译转换类:转为以 js 形式工作的模块
-
文件操作类:文件拷贝至目录,并导出访问路径
-
代码检查类:检查、统一代码风格
加载 CSS 资源 #7
css-loader:将 CSS 文件转换为 js 模块并打包
style-loader:将 CSS 模块追加进 HTML 渲染
需要在配置文件中新增 module 字段:
module: {
rules: [
{
test: /.css$/, //匹配文件路径,正则表达式
use: 'css' //使用的 loader,从下往上执行
}
]
}
且将 CSS 文件引入入口 js 文件
加载文件资源 #9
图片、字体等文件不能像 CSS 转为 JavaScript 代码,它们需要使用文件加载器 file-loader。不过它们也需要 import 到入口文件中。
【注意】Webpack 默认 HTML 文件在 output.path 的位置。如果不在,则需要配置 output.publicPath 值为输出目录相对 HTML 文件的路径,结尾保留 / 。这样,在 HTML 访问静态资源时,publicPath 拼接 loader 配置的路径方为正确路径。
加载 DataURL 资源 #10
url-loader
适用于小文件,使用 DataURL 以减少 HTTP 请求。
在配置文件 module.rules 下的 use.option 添加属性 limit,限制使用 url-loader 处理的文件大小。超过该限制大小的文件仍使用 file-loader 加载,也就是说依赖 file-loader
e.g.
module: {
rules: [
{
test: /.png$/,
use: {
loader: 'url-loader',
option: {
limit: 10 * 1024 //文件大小不超过 10 kB
}
}
}
]
}
添加对 ES6 的支持 #12
Webpack 仅是一个打包工具,本身不支持 ES6+。可以使用 babel-loader 代替默认 loader 来加载 js 文件,以实现对 ES6 的支持。
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
}
其他资源和文件的加载 #13
不同标准模块的导入
- 遵循 ES Module 标准的 import 声明
- 遵循 CommonJS 标准的 require 声明
- 遵循 AMD 标准的 define 函数和 require 函数
虽然 Webpack 支持多种标准,但是在同一项目中最好还是使用统一标准的模块。
CSS 中的 @import 和 url()
CSS 内引入的其他 CSS 文件和通过 url() 获取的文件均可正确加载
HTML 中的 <img> 的src 和 <a> 的 href
安装 html-loader 并添加一组规则:
{
test: '/.html$/',
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
默认可以处理 img 元素的 src 属性,即 options.attrs 的默认值是 'img:src'
loader 的工作原理 #15
loader 完成的即是资源文件从输入到输出的转换
loader 是一个管道的概念,对同一资源可以使用多个 loader 接续,最终的输出应是 js 代码。
Webpack 的插件 plugin
插件可以完成打包以外的其他一些自动化工作
清理目录用插件 #17
安装插件后,需要在配置文件中引入(require)插件,并在导出项的 plugins 数组内,写入插件的新建实例,如 new CleanWebpackPlugin()
clean-webpack-plugin:打包前清空输出目录
自动生成 HTML 用插件 #18~20
html-webpack-plugin:自动在输出目录生成引用了 Webpack 已捆绑包的 HTML 文件
html-webpack-plugin 可以更详细的配置
预设生成的 HTML
在新建 html-webpack-plugin 实例时,传入一个对象实参,内置一些属性可以自定义修改生成的 HTML 文件。e.g.
{
title: 'Custom Title',
meta: {
viewport: 'width=device-width'
}
}
更大的自定义可以使用模板来实现,模板内需要动态注入的部分使用 lodash 模板语法。e.g.
{
template: './src/template.html'
}
<body>
<h1><%= htmlWepackPlugin.options.title %></h1>
</body>
创建多个 HTML 文件
新建多个实例,并传入参数对象 { filename: 'about.html' } 修改文件名。
移动文件用插件
copy-webpack-plugin:将指定目录下的文件复制到输出目录,比如图标类图片。
在 plugins 下新建实例时,传入实参为要复制的目录
【工作】通常在上线前做一次静态文件并入,开发过程中能访问即可。
plugin 的工作原理 #22
通过在打包过程中的钩子上挂载函数实现扩展
Webpack Dev Server
障碍1:源代码修改后,想要在浏览器端看到效果,需要有重新编译打包和刷新页面的操作。
-
自动编译:Webpack
--watch工作模式,监听文件修改,自动打包 -
自动刷新浏览器:利用 browser-sync 上线并监听文件,自动刷新
但这样的“两步自动化”中间产生了反复的磁盘读写,拖累效率。
webpack-dev-server 是 Webpack 推出的开发工具,可以实现自动编译和自动刷新浏览器
添加对静态资源的访问 #27
config.js 配置文件 => devServer 对象 => contentBase 数组属性
e.g. devServer: { contentBase: [ './public', './base'] }
代理 API #28
开发过程页面在本地对服务器请求属于跨域请求,上线后是同源。
如果服务器不支持 CORS 跨域,那么便需要将本地代理到同源的开发用服务器上进行开发。
config.js 配置文件 => devServer 对象 => proxy 对象属性
devServer: {
proxy: {
'/api': { // 以 '/api' 开头的地址
target: 'https://api.github.com',
// 👆 https://localhost:8080 替换为 https://api.github.com
pathRewrite: { // 重写
'^/api': ''
// 👆 https://localhost:8080/api/users 修正为 https://api.github.com/users
},
changeOrigin: true
}
}
}
模块热更新 MHR
很多时候,我们需要在保留页面状态下调试代码,而页面自动刷新会导致效率降低。
模块热更新 (Hot Module Replacement) 能够实现 CSS、JS、图片等资源文件随改随上,而无需刷新。
开启 MHR #37
MHR 是 Webpack Dev Server 的内置插件。
在命令 webpack-dev-server 后加 --hot 命令行参数即可。
或配置 devServer: { hot: true },且需引入 webpack 和新建插件实例。
这时,CSS 已经可以热替换了。
进一步配置热替换逻辑 #38
【注意】通过框架生成的项目可能不需要再配置,因为框架已做好了 MHR 完整配置。
在引入模块的文件(也就是入口文件)处理热替换。
module.hot.accept('./editor', ()=>{})
module.hot 是 MHR APIs 的核心对象,它提供 accept(),第一个参数是模块 URL,第二个参数是该模块更新后的处理函数。
不同的模块需要不同的处理函数,根据模块逻辑自行书写。
js 模块 #40
e.g. 编辑器模块
let lastEditor = editor // 保留原模块结果
module.hot.accept('./editor', ()=>{
const value = lastEditor.innerHTML // 保留原状态
document.body.removeChild(lastEditor)// 移除原模块结果
const newEditor = createEditor() // 生成新模块结果
newEditor.innerHTML = value // 新模块结果得到原状态
document.body.appendChild(newEditor)
lasteditor = newEditor
})
图片模块 #41
module.hot.accept('./pic.png', ()=>{
img.src = background // 重赋一次图片地址,即实现图片刷新
})
MHR 的注意事项 #42
-
hot: true下,如果修改模块出现错误,页面自动重载修改前的模块。可使用
hotOnly: true,出错情况下页面也不重载,便于观察 bug -
为 MHR 加前置判断条件
if(module.hot),避免报错,且上线生产环境时相关代码也不会进入打包。
Source Map
障碍2:项目经过打包编译后,变量名和文件结构等都被修改,在浏览器端调试无法与源码对应。
源码地图建立打包后代码与源代码间的映射,便于直观调试。
配置 Webpack 的 Source Map #30
添加字段:devtool: 'source-map'
Source Map 的模式 #32~33
eval(类 Source Map)
devtool: 'eval'
能定位到源文件,没有 Source Map,也就没有具体行、列信息。速度最快,效果简陋。
-
带有 eval 表示使用了 eval 的执行模块代码
-
带有 cheap 表明简易,速度更高
-
带有 module 表明能得到 loader 处理前的源代码
模式选择:
开发:cheap-module-eval-source-map
生产:none / nosources-source-map
Webpack 的不同模式配置
在默认配置的基础上,添加判断修改配置。此方法适用于小型项目。
module.exports = (env, argv) => {
const config = {···}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
这样在打包时加命令行参数 --env production 即可使用生产模式配置。
大型项目使用多个配置文件更为可靠,通常有三个文件:webpack.common.js、webpack.dev.js、webpack.prod.js
common 作为基础配置文件,被 dev 和 prod 引入作为一个对象常量。const common = require('./webpack.common')
再将修改值写入。这里使用 webpack-merge 包的 merge() 处理配置对象融合。module.exports = common, {···} 。merge 函数能自动处理数组类型值的追加,而不是覆盖。
打包时需要 webpack --config webpack.prod.js,可以写进 npm scripts 使用。