具体做法:
将每个功能以及它相关的一些状态数据,单独存放到不同的文件当中,我们去约定每一个文件就是一个独立的模块。我们去使用这个模块,就是将这个模块引入到页面当中,然后直接调用模块中的成员(变量 / 函数)。一个script标签就对应一个模块,所有模块都在全局范围内工作。
缺点:
早期模块化完全依靠约定。
具体做法:
我们约定每个模块只暴露一个全局的对象,我们所有的模块成员都挂载到这个全局对象下面。
在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,有点类似于为模块内的成员添加了「命名空间」的感觉。
缺点:
具体做法:
使用立即执行函数的方式,去为我们的模块提供私有空间。将模块中每个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员,我们可以通过挂载到全局对象上的这种方式去实现。确保了私有成员的安全。
有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。
具体做法:
在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项。
这使得每一个模块之间的关系变得更加明显。
以上四个阶段就是早期在没有工具和规范的情况下,通过约定的方式,对模块化的落地方式。
我们需要的是模块化标准+模块加载器。
CommonJS规范(nodeJS):
CommonJS约定的是以同步模式加载模块,在浏览器端使用会导致效率低下。
AMD:(异步的模块定义规范)
Require.js实现了这个规范。
目前绝大多数的第三方库都支持AMD规范。
Sea.js+CMD
类似CommonJS规范,使用上跟Require.js差不多。
通过给script添加type=module的属性,就可以以ES Module 的标准执行其中的JS代码。
<script type="module">
console.log('This is es module.'); //This is es module.
</script>
ES Modules基本特性
<script>
console.log(this); //Window
</script>
<script type="module">
console.log(this); //undefined
</script>
<script type="module">
var bar = 88
console.log(bar); //88
</script>
<script type="module">
console.log(bar); //Uncaught ReferenceError: bar is not defined
</script>
<script type="module" src="https://libs.baidu.com/jquery/2.0.0/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://unpkg.com/[email protected]/dist/jquery.min.js"></script>
<!-- 成功请求到 -->
<script type="module" src="demo.js"></script>
<p>hello world</p>
ES Modules导入和导出
//导出
const bar = 'hello'
export {bar}
//导入
import {foo} from './module.js'
console.log(foo)
注意:
ES Modules导入的注意事项:
import { name, age } from './module.js'
import {} form './module.js'
import './module.js' //简写法
import * as obj from './module.js'
console.log(obj.name);
import('./module.js').then(module => {
console.log(module);
})
//导出默认成员和命名成员
export { name, age }
export default 'default export'
//导入默认成员和命名成员
import { name, age, default as title } from './module.js'
//简写
import title, { name, age } from './module.js'
ES Modules导出导入成员:
// import { Button } from './button.js'
// import { Avatar } from './avatar.js'
// export { Button, Avatar }
//简化-导出导入成员方式
export { default as Button } from './button.js'
export { Avatar } from './avatar.js'
ES Modules浏览器环境Polyfill
<script nomodule src="https://unpkg.com/[email protected]/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/[email protected]/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/[email protected]/dist/browser-es-module-loader.js"></script>
<script type="module">
import { foo } from './module.js'
console.log(foo);
</script>
ES Modules in Node.js
import { foo, bar } from './module.mjs'
console.log(foo, bar); //hello china
//载入原生模块
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'ES Module working')
//系统内置模块兼容了 ESM 的提取成员方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'ES Module working')
//载入第三方模块
import _ from 'lodash'
console.log(_.camelCase('ES Module')); //esModule
//不支持,因为第三方模块都是导出默认值
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module')); //SyntaxError: The requested module 'lodash' does not provide an export named 'camelCase'
//运行命令
// node --experimental-modules index.mjs
ES Modules in Node.js与CommonJS交互
注意事项:
ES Modules in Node.js与CommonJS的差异
引入模块化后,带来的新问题:
无容置疑,模块化是必要的。
预期的工具需要满足以下设想:
打包工具解决的是前端整体的模块化,并不单指JavaScript模块化。
Webpack
Webpack配置
Webpack 4 以后支持零配置方式直接启动打包,整个打包过程会按照约定将src/index.js作为打包的入口,最终打包结果会存放到dist/main.js中。
大多时候,我们需要自定义配置文件:
Webpack资源模块加载
Webpack内部默认只会处理JavaScript文件,其他类型文件,我们需要去添加不同类型的loader。
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
//其他类型文件加载器的规则配置
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}
Loader是Webpack的核心特性,借助于Loader就可以加载任何类型的资源。
Webpack导入资源模块
Webpack建议我们在编写代码过程中,根据代码的需要动态导入资源,因为需要资源的不是应用,而是代码。
JavaScript驱动了整个前端应用:
Webpack文件资源加载器
用来加载图片、字体等资源文件。通过拷贝物理文件的形式去处理文件资源。
WebpackURL加载器
我们可以通过Data URLs形式去表示文件,如:
Data URLs是一种特殊的URL协议,可以用来直接去表示一个文件。传统URL要求服务器上有一个对应的文件,通过请求这个地址得到服务器上对应的这个文件。Data URLs是一种当前URL就可以直接去表示文件内容的方式,这种URL当中的文本就已经包含了文本的内容。使用这种URL时,就不会再去发送任何的HTTP请求。
加载器:url-loader
最佳实践:
可配置:
rules:[
{
test: /.jpg$/,
//use: 'url-loader'//'file-loader'
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 //10KB
}
}
}
]
Webpack常用加载器分类
Webpack处理ES2015
因为模块打包需要,所以Webpack默认就可以处理import和export,但是它并不能转换代码中其他的ES6特性。
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
Webpack加载资源的方式
Loader加载的非JavaScript也会触发资源加载,如样式中的@import指令和url函数,HTML代码中图片标签的src属性。
Webpack核心工作原理
Loader机制是Webpack的核心。
Loader负责资源文件从输入到输出的转换。对于同一个资源可以依次使用多个Loader,管道概念。
Loader专注实现资源模块加载,Plugin解决其他自动化工作。
Webpack+Plugin,实现大多前端工程化工作。
插件机制是Webpack当中另外一个核心特性,目标是增强Webpack自动化能力。
Loader专注实现资源模块加载,Plugin解决其他自动化工作。
相比Loader,Plugin拥有更宽的能力范围。
Plugin通过钩子机制实现,类似于web中的事件。
Webpack+Plugin,实现大多前端工程化工作。
Webpack
Webpack常用插件
Webpack开发一个插件
Webpack要求我们的插件必须是一个函数或者是一个包含apply方法的对象。
//移除Webpack注释的插件
class VioletPlugin {
apply(compiler) {
console.log('VioletPlugin 启动');
compiler.hooks.emit.tap('VioletPlugin', 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 VioletPlugin()
]
Webpack插件是通过在生命周期的钩子中挂载函数实现扩展。
Webpack开发体验
理想的开发环境:
如何增强Webpack开发体验:
Webpack Dev Server静态资源访问:Dev Server默认只会serve打包输出文件,只要是Webpack 输出的文件,都可以直接被访问,其他静态资源文件也需要serve。
Webpack配置Source Map
在webpack.config.js文件中添加devtool: ‘下表中的方式’。
devtool模式对比:
Source Map会暴露源代码,建议生产模式选择none。
Webpack HMR体验
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'source-map',
devServer: {
hot: true
// hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
},
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()
]
}
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// ============ JS模块热替换 ============
// console.log(createEditor)
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
})
}
(2.)图片模块热替换
module.hot.accept('./better.png', () => {
img.src = background
console.log(background)
})
Webpack 不同环境下的配置
生产环境注重运行效率,开发环境注重开发效率,webpack 4提供了mode属性。
创建 不同环境下的配置方式:
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: '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
}
Webpack DefinePlugin
为代码注入全局成员。process.env.NODE_ENV
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
Webpack Tree-shaking
[摇掉]代码中未引用部分。生产模式下会自动开启。
使用前提是必须是ES Modules,即由Webpack 打包的代码必须使用ESM。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
// 模块只导出被使用的成员,usedExports负责标记【枯树叶】
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果,minimize负责【摇掉】【枯树叶】
// minimize: true
}
}
Webpack sideEffects
允许我们通过配置的方式去标识代码是否有副作用,从而为Tree-shaking提供更大的压缩空间。
副作用:模块执行时除了导出成员之外所作的事情。
sideEffects一般用于npm包标记是否有副作用。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
optimization: {
sideEffects: true
}
}
Webpack 代码分割
所有代码最终都被打包到一起,bundle体积过大,应用使用时,并不是每个模块在启动时都是必要的,浪费流量和带宽,因此需要分包,按需加载。
Code Splitting:代码分包/代码分割
HTTP1.1本身缺陷:
目前,Webpack实现代码分割/分包的方式:
Webpack 输出文件名Hash
一般我们去部署前端的资源文件时,都会去启用服务器的静态资源缓存。
生产模式下,文件名使用Hash。
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin({
// filename: '[name]-[hash].bundle.css' //项目级别Hash
filename: '[name]-[chunkhash].bundle.css' //chunk级别
// filename: '[name]-[contenthash:8].bundle.css' //文件级别
})
]
Webpack大而全,Rollup小而美。
Rollup仅仅是一款ESM打包器,Rollup并不支持类似HMR这种高级特性。
Rollup初衷只是想提供一个充分利用ESM各项特性的高效打包器。
rollup和webpack的区别:
rollup基本用法
1.创建目录
2.创建文件
3.package.json配置项
{
"name": "rollup_demo",
"version": "1.0.0",
"description": "",
"main": "rollup.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"rollup": "rollup -c rollup.config.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.7.0",
"babel-preset-latest": "^6.24.1",
"rollup": "^0.64.1",
"rollup-plugin-babel": "^3.0.7",
"rollup-plugin-node-resolve": "^3.3.0"
}
}
4.rollup.config.js配置
import babel from 'rollup-plugin-babel'
import resolve from 'rollup-plugin-node-resolve'
import { format } from 'path';
export default{
entry:'src/index.js', //入口
format:'umd', //兼容 规范 script导入 amd commonjs
plugins:[
resolve(),
babel({
exclude:'node_modules/**'
})
],
dest:'build/bundle.js'
}
5.运行 npm run rollup
Parcel