模块化
- 演变过程
阶段一. 按文件划分,每个文件就是一个独立模块,代码中调用全局成员
问题:污染全局 命名冲突 完全依靠约定
阶段二: 命名空间 每个模块只暴露一个全局对象,模块成员挂到对象下面
问题: 没有私有空间,模块成员仍然可以被修改,模块依赖关系问题
阶段三: 立即执行函数内部挂载对象,对象上挂载对外成员,私有成员在函数内部
commonjs标准
- 一个文件都是一个模块
- 每个模块都有单独作用域
- module.exports导出成员
- require载入模块
- 同步模式加载模块
AMD
ESModule
- 自动采用严格模式
- 每个模块都要单独作用域
- 通过cors去请求外部js
- script标签延迟执行
ES Module - 模块的特性
需要显示的内容
export import
- export { }是固定写法,不是导出对象的意思
- export default 后面可以接不同类型变量
- import { } 对应export
- import name 对应 export default
- 对外暴露的成员暴露的是引用关系
- 导入的成员是只读成员
- import 引入文件名路径要完成不能省略,相对路径不能省略,省略会认为在加载第三方模块,可以是绝对路径 url
- import {} from './module' 或者 import './module' 执行模块, 而不提取具体成员
- import * as obj from './module'导入所有模块,所有模块存储在对象中
- import from不能动态导入 ,可以使用import()函数动态导入,返回一个promise
import('./module').then(module=>{
console.log(module)
})
默认导出与export导出同时存在
import {a,b,c as default} from './modules'
简写 import c, {a,b} from './modules'导入模块作为导出成员 export {a,b} from './module'
script type="nomodule" //不支持 ES Module的浏览器才会执行
es中可以导入commonjs模块,作为默认导出,反之不可以
webpack
- npm init初始化package.json文件
- 使用 cnpm i webpack webpack-cli -D
- npx webpack 默认按照 src/index.js打包
- 自定义配置 项目下新建 webpack.config.js
- webpack --mode development/production/none 开发模式/生产模式打包/原始状态打包 也可以在配置文件中设置 mode属性
const path = require('path')
module.exports = {
// 这个属性有三种取值,分别是 production、development 和 none。
// 1. 生产模式下,Webpack 会自动优化打包结果;
// 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
// 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
资源加载
- 加载其他资源通过指定对应loader来解析对应文件
- module属性 配置 rules来指定对其他资源加载规则
- rules中可以指定多个loader,从后往前执行 css-loader把css转换为js模块,style-loader把对应模块以style标签形式插入html中
var path = require("path")
module.exports = {
mode:'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, "./dist"),
filename: '.js/bundle.[name].[hash].js'
},
module:{
rules:[
{
test:/.css$/,
use:['style-loader','css-loader']
}
]
}
}
文件资源 loader
- 加载图片 cnpm i file-loader -D
- base64加载资源 cnpm i url-loader -D
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit:10*1024,//10k一下url-loader处理 以上自动使用file-loader
esModule: false
}
}
],
常用loader
- 编译转换类loader ex. css-loader => css转换为js模块
- 文件操作类型loader 把文件输出到对应目录,同时导出文件的访问路径 ex.file-loader
- 代码检查loader 统一代码风格 eslint-loader
webpack 加载资源的方式
- 兼容 Es module CommonJs AMD
- 加载的非js也会触发资源加载对应的loader ex. css中 @import url img的src
- html-loader a标签href默认不会触发资源加载,可以在options只配置
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
webpack工作原理
- 入口js中通过import/require找到对应的文件依赖,分别取解析每一个资源模块对应的依赖,形成一个依赖树,递归资源树,找到对应对应资源配置的加载器去加载对应模块,最后把加载结果放到bundle.js
es6转换
- cnpm i babel-loader @babel/core @babel/preset-env -D
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
自定义loader
- 项目根目录新建markdown-loader.js
- loader返回结果必须是一段js代码,方便交给下一个loader处理
- 负责输入到输出的转换 类型管道,同一个资源可以使用多个loader
const marked = require('marked')
module.exports = source => {
//source接受输入
const html = marked(source)
// 返回 html 字符串 交给下一个 html-oader 处理
return html
}
webpack.config.js配置
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./markdown-loader' //从后往前执行
]
}
]
plugin
- 解决除了资源加载之外的自动化工作 ex:清除目录 压缩代码 拷贝静态资源到输出目录
- 常用插件 clean-webpack-plugin(清除目录) html-webpack-plugin(生成html)
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
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: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
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([
// 'public/**'
'public'
])
]
}
自定义插件 钩子机制
- 通过在声明周期钩子中挂载函数来扩展
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 启动')
//webpack即将往输出目录输出文件执行
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
}
}
}
})
}
}
监视模式运行 webpack --watch
- 编译自动刷新 cnpm i webpack-dev-server
- npx webpack-dev-server --open 自动运行webpack进行打包并启动开发服务器
此时会监听文件变化自动刷新浏览器
并不会生成dist目录,打包结果暂存在内存中,http-server从内存中读取资源发送给浏览器解析 - devServer配置静态资源访问路径 一般是开发阶段配置静态资源访问路径,生成上线打包public目录
devServer:{
contentBase:'./public'
},
代理
devServer:{
proxy:{
'/api':{
target:'https://xxxxx',
//路径重写
pathRewrite:{
'^api':''
},
//使用实际请求的地址作为主机名
changeOrigin:true
}
}
},
sourceMap
- 解决编写代码与运行代码不一致产生的调试问题
- jquery末尾 中 添加 //# sourceMappingURL=jquery-3.4.1.min.map生成源码
- webpack 配置 devtool:'source-map'
sourceMap 类型
- eval模式 找问题代码是打包过后的模块代码,每个模块转换过后的代码放在eval函数中执行 执行最后通过 //# sourceURL = xx指定源码地址 定位错误出现的文件,不会生成sourceMap文件,构建速度最快,但无法直接找到错误具体行信息
- eval-source-map 想必eval生成了sourceMap ,可以查看错误信息行列信息
- cheap-eval-source-map 比eval-source-map轻量 只能看到错误行信息,速度更多
- cheap-module-eval-source-map 于cheap-eval-source-map 想比显示的是没有经过es6编辑的代码
- inline-source-map 把sourceMap以base64嵌入源代码中
- hidden-source-map 生成了sourceMap文件,代码中并没有引入对应map文件
- nosource-source-map 能看到错误的行列信息,但看不到源代码
- webpack.config.js可以导出一个数组,产生多少打包结果
选择souceMap
- 开发环境 cheap-module-eval-source-map
- 生成环境 none/nosource-source-map
//相同入口可以生成 a.js b.js
module.exports = [
{
entry: './src/main.js',
output: {
filename: 'a.js'
}
},
{
entry: './src/main.js',
output: {
filename: 'b.js'
}
}
]
HMR
- 模块热更新 页面不刷新情况下,更新模块
- 集成在webpack-dev-server中 webpack-dev-sercer --hot开启或者在配置文件中配置
- webpack-dev-server hot选项设置为true plugin中
new webpack.HotModuleReplacementPlugin() - js文件热更新需要自定义处理模块替换逻辑,默认会自动刷新,样式文件经过loader处理,loader中处理过了热更新,不需要自定义处理
//注册对应模块发生变化的处理函数
module.hot.accept('./edit',function(){
处理逻辑
})
hot改为hotOnly 出现错误,不会自动刷新
不同环境下配置
1.配置文件中添加判断
配置文件导出一个函数 函数中返回配置项
接受两个参数,一个是cli 命名传递的参数 一个是运行传递的所有参数
-- npx webpack --env production 传递参数打包生成环境
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
}
2 多配置文件方式
- webpack.dev.js webpack.common.js webpack.prod.js
- webpack-merge追加新的配置
- npx webpack --config 配置文件名 进行打包
common
const path = require("path")
const webpack = require('webpack')
const merge = require('webpack-merge')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
devServer:{
contentBase:'./public'
},
entry: './src/main.js',
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.vue$/,
loader: ['vue-loader'],
exclude: /node_modules/,
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
],
exclude: /node_modules/,
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024,//10k一下url-loader处理 以上自动使用file-loader
esModule: false
}
}
],
exclude: /node_modules/,
}
]
},
plugins: [
new CleanWebpackPlugin(),
new VueLoaderPlugin(),
new htmlWebpackPlugin({
title: 'spa单页应用',
url: '',
filename: 'index.html',
template: './index.html'
})
],
}
dev
// 开发环境配置
const merge = require('webpack-merge') // webpack 合并配置插件 详细了解==>(https://github.com/survivejs/webpack-merge)
const common = require('./webpack.common.js') // 引入公共模块配置
const webpack = require('webpack') // 引入webpack
module.exports = merge(common, {
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist'
},
mode: 'development',
})
prod
// 配置生产环境
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
devtool:'none',
plugins: [
new UglifyJSPlugin()
new CopyWebpackPlugin(
{
patterns: [
{
from: path.resolve(__dirname, './public'), //定义要拷贝的源目录,必填项
to: 'public' //定义要拷贝到的目标目录,非必填,不填写则拷贝到打包的output输出地址中
}
]
}
),
],
})
DefinePlugin
- 向全局注入全局变量
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')
})
]
}
tree-shaking
- 去掉未引用代码
- babel-loader使用失效,tree-shaking处理的必须是esModule
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
- 模块执行时除了导出成员之外所做的事情
- 一般用于npm包标记是否有副作用
- package.json配置 "sideEffects":"false" 标识项目代码没有副作用
- 一个组件a.js文件引入所有组件,另一个b.js引入了一个a.js中的一个组件,但由于a.js引入了所有组件,打包就会打包所有组件,为了解决这个问题,采用副作用处理,没有用到的组件模块就不会打包
- 要确定项目的副作用,否则使用该功能会误删有作用的代码
- package.json 可以设置哪些文件有副作用
"sideEffects": [
"./src/extend.js",
"*.css"
]
optimization: {
sideEffects: true, //开启副作用功能
// 模块只导出被使用的成员
// usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true,
}
代码分割
- 应用比较大时,所有模块打包到一起,会造成体积很大影响加载速度
- 分包 按需加载 采用动态导入或者多入口打包来解决
- 多入口打包
- entry采用对象写法配置多入口
- output filename: '[name].bundle.js' //动态输出文件名 name最后会被替换为入口文件的名称
- HtmlWebpackPlugin 指定chunk来指定输出html的对应打包js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' //动态输出文件名 name最后会被替换为入口文件的名称
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
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'
}
},
按需加载
- 需要某个模块才去加载对应模块
- 采用动态导入,会被自动分包 import('xx').then(module=>{ })
- /* webpackChunkName: 'components' */ 给分包起名,默认是数字,如果名字起一样,就会被打包到一个文件中
if (hash === '#posts') {
// mainElement.appendChild(posts())
import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'album' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
MiniCssExtractPlugin
- 提取css到单独文件
- css文件超过一定体积如150kb,才需要考虑提取文件
- webpack默认压缩不会压缩css需要单独插件optimize-css-assets-webpack-plugin
- terser-webpack-plugin防止css压缩覆盖js压缩
- minimizer生成环境默认开启,下面对应插件才会运行
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
文件hash
- 生产环境资源文件配置hash防止文件修改缓存
- output与plugin filename 一般支持hash
- [name]-[hash].bound.css 项目级别hash 只要项目任何地方改动,当前hash就会发生改变
- [name]-[chunkhash].bound.css 同一路的文件发生改变,hash才会改变
- [name]-[contenthash].bound.css 文件hash 文件发生改变才会触发hash改变
- [name]-[contenthash:8].bound.css :数字指定hash长度