模块化笔记

359 阅读15分钟

内容概要

  • 1、模块化演变过程
  • 2、模块化规范
  • 3、常用的模块化打包工具
  • 4、基于模块化工具构建现代Web应用
  • 5、打包工具的优化技巧

1、模块化演变过程

文件划分方式

  • 污染全局作用域
  • 命名冲突问题
  • 无法管理模块依赖关系 原始方式完全依靠约定

命名空间方式

IIFE

模块化规范 + 模块加载器

CommonJS
  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过 module.exports 导出成员
  • 通过 require 函数载入模块
CommonJS 是以同步模式加载模块
AMD(Asynchronous Module Definition)
  • AMD 使用起来相对复杂
  • 模块 JS 文件请求频繁
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()
})
Sea.js + CMD
// 兼容 CMD 规范(类似 CommonJS 规范)
define(function (require, exports, module) {
    // 通过 require 引入依赖
var $ = require('jquery')
// 通过 exports 或者 module.exports 对外暴露成员
module.exports = function () {
    console.log('module 2~')
    $('body').append('<p>module2</p>')
}
})
参考网址

AMD 和 CMD 的区别有哪些? SeaJS 和 RequireJS 的差异 模块化开发之AMD规范 模块化开发之CMD规范 模块化开发之CommonJS规范 ES6中Module语法与加载实现

2、模块化规范

  • CommonJS in node.js
  • ES Modules in Browers

ES Modules 特性

  • serve .

特性清单

  • 自动采用严格模式,忽略‘use strict’
  • 每个ESM模块都是单独的私有作用域
  • ESM 是通过 CORS 去请求外部 JS 模块的
  • ESM 的 script 标签会延迟执行脚本 defer

ES Modules 导入和导出

  • browser-sync . --file **/*.js

导出里面可以给变量重命名用 as ,对应导入也要用新的重命名的名字。 注意,default是关键字。

ES Modules 导入和导出 注意事项

{ name, hello } 不是一个对象字面量

export default { name, age } 这个是字面量对象

导出成员的引用,内存空间引用,引用关系给到外部

// 它只是语法上的规则而已
export { name, age }

ES Modules 导入和导出 导入import

​ 1、路径:./相对路径;/绝对路径; 完整的url;

2、加载模块并不提取它,不需要外界控制的子功能模块
// import {} from './module.js'
// import './module.js'

3、提取所有成员
// import * as mod from './module.js'
// console.log(mod)

4、动态导入模块
// import('./module.js').then(function (module) {
//   console.log(module)
// })

5、命名成员和默认成员
// import { name, age, default as title } from './module.js'
import abc, { name, age } from './module.js'  //逗号左边默认成员
console.log(name, age, abc)

ES Modules 导入和导出 直接导出所导入的成员

​ 在组件文件夹下面创建index.js用来放全部的导出文件,注意default

ES Modules in Browser Polyfill兼容方案

​ Browser ES Module Loader ​ node地址获取:unpkg.com/XXX 2个

IE : Promise Polyfill          1个

<script nomodule></script>  nomodule,在不支持的浏览器上工作

只适合开发阶段,不适合生产阶段

ES Modules in Node.js

​ 1、创建.mjs文件 ​ cd XX 进入相应的目录 ​ 2、- node --experimental-modules index.mjs //实验特性

原生模块
第三方模块
- yarn add lodash
直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式

ES Modules in Node.js 与 CommonJS 模块交互

  • ES Module 中可以导入 CommonJS 模块
  • CommonJS 中不能导入 ES Module 模块
  • CommonJS 始终只会导出一个默认成员
  • 注意 import 不是解构导出对象

ES Modules in Node.js 与 CommonJS 模块的差异

​ nodemon --experimental-modules esm.mjs

// require, module, exports 自然是通过 import 和 export 代替

// __filename 和 __dirname 通过 import 对象的 meta 属性获取
// const currentUrl = import.meta.url
// console.log(currentUrl)

// 通过 url 模块的 fileURLToPath 方法转换为路径

ES Modules in Node.js 新版本进一步支持 ESM

// Node v12 之后的版本,可以通过 package.json 中添加 type 字段为 module,
// 将默认模块系统修改为 ES Module
// 此时就不需要修改文件扩展名为 .mjs 了

// 如果需要在 type=module 的情况下继续使用 CommonJS,
// 需要将文件扩展名修改为 .cjs

ES Modules in Node.js Babel 兼容方案

- yarn add @babel/node @babel/core @babel/preset-env --dev
- yarn babel-node
- yarn babel-node index.js --presets=@babel/preset-env
- yarn remove @babel/preset-env
- yarn add @babel/plugin-transform-modules-common.js --dev
- yarn babel-node index.js


preset 就是一组插件

3、常用的模块化打包工具

ES Modules 存在环境兼容问题
模块文件过多,网络请求频繁
所有的前端资源都需要模块化

代码编译:开发阶段的 es6 编译 生产阶段的 es5
模块打包:开发阶段的 es6 打包 生产阶段的 Bundle.js
多类型模块支持: 开发阶段的 (.js .css .scss .hbs .png .ts) 打包 生产阶段的 (Bundle.js .css .png)
  • 新特性代码编译
  • 模块化 JavaScript 打包
  • 支持不同类型的资源模块

模块打包工具 概要

​ webpack ​ 模块加载器 (Loader) ​ 代码拆分 (Code Splitting) ​ 资源模块 (Asset Module)

打包工具解决的是前端整体的模块化,并不单值 Javascript 模块化

在安装一个要打包到生产环境的安装包时,你应该使用 npm install --save,如果你在安装一个用于开发环境的安装包(例如,linter, 测试库等),你应该使用 npm install --save-dev。请在 [npm 文档](https://docs.npmjs.com/cli/install) 中查找更多信息。

Webpack 快速上手

  • yarn init --yes

  • yarn add webpack webpack-cli --dev

  • yarn webpack --version

  • yarn webpack

    "scripts": { "build": "webpack" //直接用build命令执行 },

    不需要type="module",直接引用生成的dist下的目录文件

Webpack 配置文件

​ 'src/index.js' -> 'dist/main.js' ​ 根目录下创建webpack.config.js

const path = require('path')

module.exports = {
    entry: './src/main.js',   //指定入口文件路径,相对路径./是不能省略的
    output: {     //输出文件的位置
        filename: 'bundle.js',    //输出对象的名称
        path: path.join(__dirname, 'output')  //输出对象的绝对路径
    }
}

Webpack 工作模式

  • 这个属性有三种取值,分别是 production、development 和 none。
  • 生产模式下,自动优化打包结果;
  • 开发模式下,自动优化打包速度,添加一些调试过程中的辅助;
  • None 模式下,运行最原始的打包,不做任何额外处理
  • yarn webpack

  • yarn webpack --mode development

  • yarn webpack --mode none

    参考网址:webpack.js.org/configurati… 或者通过mode: 'development', 去指定

Webpack 打包结果运行原理

​ 快速折叠 Ctrl+k Ctrl+0

Webpack 资源模块加载

  • yarn add css-loader --dev

  • yarn add style-loader --dev

    module: { rules: [ { test: /.css$/, use: [ //多个loader,从后往前执行 'style-loader', 'css-loader' ] } ] }

    Loader 是 webpack 的核心特性 借助于Loader 就可以加载任何类型的资源

Webpack 导入资源模块

根据代码的需要动态导入资源
需要资源的不是应用,而是代码
Javascript 驱动整个前端应用
  • 逻辑合理,JS确实需要这些资源文件

  • 确保上线资源不缺失,都是必要的

    新事物的思想才是突破点

Webpack 文件资源加载器 Data URLs 与 url-loader

file-loader { test: /.png$/, use: 'file-loader' }

  • yarn add file-loader --dev publicPath: 'dist/' //网站的根目录

    Data URLs
    
    data: [<mediatype>][;base64], <data>
    协议; 媒体类型和编码           文件内容
    
    data:text/html;charset=UTF-8,<h1>html content</h1>
    
    data:image/png;base64,iVBORw0KGgoAAAANSUhE...SuQmCC
    

url-loader

  • yarn add url-loader --dev

  • 小文件使用 Data URLs, 减少请求次数

  • 大文件单独提取存取,提高加载速度

  • 超出10kb 文件单独提取存放

  • 小于10kb 文件转换为 Data URLs 嵌入代码中 { test: /.png$/, use: { loader: 'url-loader', //适合小文件 options: { //添加配置属性 limit: 10 * 1024 // 10 KB 以下的文件转换,以上的文件交给file-loader } } }

Webpack 常用加载器分类

​ 编译转换类 ​ foo.css css-loader bundle.js 以JS形式工作的CSS模块

文件操作类
bar.png  file-loader  bundle.js  导出文件访问路径  bar.png

代码检查类
baz.js   eslint-loader 检查通过/不通过  baz.js

Webpack 与 ES2015

​ 因为模块打包需要,所以处理import 和 export

  • yarn add babel-loader @babel/core @babel/preset-env --dev

  • webpack 只是打包工具

  • 加载器可以用来编译转换代码

    { test: /.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] //这个集合包含了全部的最新的es特性 } } },

Webpack 模块的加载方式

遵循 ES Modules 标准的 import 声明
遵循 CommonJS 标准的 require 函数
遵循 AMD 标准的 define 函数和 require 函数

样式代码中@import 指令和 url函数
HTML 代码中图片标签的 src 属性
  • yarn add html-loader --dev

    { test: /.html$/, use: { loader: 'html-loader', options: { attrs: ['img:src', 'a:href'] } } }

Webpack 核心工作原理

​ bundle your assets

Loader 机制是 Webpack 的核心

Webpack Loader的工作原理

markdown-loader

module.exports=""

Source -> md-loader -> (other-loader) -> Result(javascript代码)
  • yarn add marked --dev

  • yarn add html-loader --dev

    { test: /.md$/, use: [ 'html-loader', './markdown-loader' ] }

    const marked = require('marked')

    module.exports = source => {
        // console.log(source)
        // return 'console.log("hello ~")'
        const html = marked(source)
        // return html
        // return `module.exports = "${html}"`   
        // return `export default ${JSON.stringify(html)}`   先将字符串转换为标准的json字符串

        // 返回 html 字符串交给下一个 loader 处理
        return html
    }
Loader 负责资源文件从输入到输出的转换
对于同一个资源可以依次使用多个Loader
css-loader -> style-loader

Webpack 插件机制

​ 目的:增强webpack 自动化能力 ​ Loader 专注实现资源模块加载 ​ Plugin 解决其他自动化工作 ​ e.g. 清除 dist 目录 ​ e.g. 拷贝静态文件至输出目录 ​ e.g. 压缩输出代码 ​ webpack + Plugin 实现大多前端工程化工作

Webpack 常用插件 clean-webpack-plugin

​ 自动清理输出目录的插件,如dist目录

  • yarn add clean-webpack-plugin --dev

    const { CleanWebpackPlugin } = require('clean-webpack-plugin')

    plugins: [ //专门配置插件的地方 new CleanWebpackPlugin() //创建一个实例,放到数组中 ]

Webpack 常用插件 html-webpack-plugin

​ 选项 ​ 多实例 ​ 通过webpack 输出 HTML 文件

  • yarn add html-webpack-plugin --dev
const HtmlWebpackPlugin = require('html-webpack-plugin')
// publicPath: 'dist/'  这个配置就不需要了
根目录下就不需要HTML文件了

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'
    })
]

同时输出多个页面文件

plugins: [
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
    title: 'Webpack Plugin Sample',
    meta: {
        viewport: 'width=device-width'
    },
    template: './src/index.html'    //HTML模板文件
    }),
    // 用于生成 about.html
    new HtmlWebpackPlugin({     //加入多个实例对象,生成多个HTML文件
    filename: 'about.html'
    })
]

Webpack 常用插件 copy-webpack-plugin & 总结

  • yarn add copy-webpack-plugin --dev
    const CopyWebpackPlugin = require('copy-webpack-plugin')

    plugins: [
        new CleanWebpackPlugin(),
        // 用于生成 index.html
        new HtmlWebpackPlugin({
        title: 'Webpack Plugin Sample',
        meta: {
            viewport: 'width=device-width'
        },
        template: './src/index.html'    //HTML模板文件
        }),
        // 用于生成 about.html      加入多个实例对象
        new HtmlWebpackPlugin({
        filename: 'about.html'
        }),
        new CopyWebpackPlugin([
        // 'public/**'
        'public'    //public目录下所有文件拷贝到输出目录
        ])
    ]
社区还提供了很多插件
需求 -》 关键词 -》 搜索

开发一个插件

​ 相比于 Loader, Plugin拥有更宽的能力范围 ​ Plugin 通过钩子机制实现 ​ 一个函数或者是一个包含 apply 方法的对象

class MyPlugin {    //移除webpage注释插件的过程
    apply (compiler) {
        console.log('MyPlugin 启动')

        compiler.hooks.emit.tap('MyPlugin', compilation => {    //hooks访问,tap方法注册钩子函数
        // 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内部要求必须的方法
            }
            }
        }
        })
    }
}

new MyPlugin()

通过在生命周期的钩子中挂载函数实现扩展

多个任务

Webpack 开发体验

​ 编写源代码 webpack 打包 运行应用 刷新浏览器 会降低开发效率 ​ 设想开发环境: ​ 1.以HTTP Server 运行 ​ 2.自动编译 + 自动刷新 ​ 3.提供 Source Map 支持

Webpack 增强开发体验

​ 自动编译 ​ watch 工作模式 ​ 监听文件变化,自动重新打包 - yarn webpack --watch 以监视模式去运行 - serve dist 以http的形式运行应用

自动刷新浏览器
    BrowserSync
    -   browser-sync dist --files "**/*"
        操作上麻烦了,效率上降低了,多出2不磁盘读写操作

Webpack Dev Server

​ 提供用于开发的 HTTP Server ​ 集成[自动编译] 和 [自动刷新浏览器] 等功能

  • yarn add webpack-dev-server --dev

  • yarn webpack-dev-server 运行命令 将打包结果暂时存放到内存当中

  • yarn webpack-dev-server --open 自动唤醒浏览器,打开页面地址

    可以一边编码,一边及时预览

Webpack Dev Server静态资源访问

​ Dev Server 默认只会serve打包输出文件 ​ 只要是webpack 输出的文件,都可以直接被访问 ​ 其他静态资源文件也需要 serve

contentBase
额外为开发服务器指定查找资源目录
devServer: {
    contentBase: './public',    //指定额外的静态资源路径,字符串或者数组
}

// // 开发阶段最好不要使用这个插件
// new CopyWebpackPlugin(['public'])    留在上线前的那次打包中使用

Webpack Dev Server代理API服务

​ 跨域资源共享(CORS) ​ 使用 CORS 的前提是API必须支持 ​ 并不是任何情况下 API 都应该支持 ​ 如果前后端 同源部署, 域名,协议,端口

问题:开发阶段接口跨域问题
Webpack Dev Server 支持配置代理
目标:将 GitHub API 代理到开发服务器
Endpoint 可以理解为 接口端点/入口
    devServer: {
        contentBase: './public',    //指定额外的静态资源路径,字符串或者数组
        proxy: {    //添加代理服务配置
        '/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': ''
            },
            // 不能使用 localhost:8080 作为请求 GitHub 的主机名
            changeOrigin: true    //以实际代理的主机名作为请求
        }
        }
    },

    // ======================== fetch proxy api example  main.js========================

    const ul = document.createElement('ul')
    document.body.append(ul)

    // 跨域请求,虽然 GitHub 支持 CORS,但是不是每个服务端都应该支持。
    // fetch('https://api.github.com/users')
    fetch('/api/users') // http://localhost:8080/api/users
    .then(res => res.json())
    .then(data => {
        data.forEach(item => {
        const li = document.createElement('li')
        li.textContent = item.login
        ul.append(li)
        })
    })

(https://www.example.com/index.html)
|
v
(https://www.example.com/api/users)

(https://localhost/index.html)
|
v
(https://www.example.com/api/users)

Source Map

​ 运行代码与源代码之间完全不同,如果需要调试应用,错误信息无法定位,调试和报错都是基于运行代码 ​ source Source Map(映射关系) Compoled

//# sourceMappingURL=jquery-3.4.1.min.map   放在.min.js最后面,自动请求这个文件生成.min.map,根据这个文件的内容逆向解析出来的源代码,以便调试

Source Map 解决了源代码与运行代码不一致所产生的问题

Webpack 配置Source Map

​ devtool: 'source-map', //与Source Map相关的功能配置 ​ webpack支持12种不同的打包方式,每种方式的效率和效果各不相同

eval 模式下的Source Map
devtool: 'eval',
打包后端模块代码,知道对应的源代码,只能定位源代码的名称不知道行内信息

不同 devtool 之间的差异

​ 准备工作 ​ 具体对比 ​ const HtmlWebpackPlugin = require('html-webpack-plugin') //给每个打包生成HTML文件

const allModes = [
    'eval',		//将模块代码放到eval函数当中去执行,通过source url标注文件的路径,并没有生成对应的source map,只能定位哪个文件出了错误
    'cheap-eval-source-map',		//eval函数当中去执行,可以定位错误的文件,只能定位行,经过es6转换后的结果,加工
    'cheap-module-eval-source-map',		//eval函数当中去执行,可以定位错误的文件,只能定位行,定位源代码跟我们编写的是一摸一样的;module没有经过loader加工
    'eval-source-map',		//eval函数当中去执行,可以定位错误的文件,还可以定位行和列错误的信息,生成对应的source map
    'cheap-source-map',		//没有eval方式去执行模块代码,没有module经过loader处理过
    'cheap-module-source-map',
    'inline-cheap-source-map',
    'inline-cheap-module-source-map',
    'source-map',
    'inline-source-map',	//data url方式嵌入,最不可能用到的
    'hidden-source-map',	//看不到source-map的效果,确实生成了source-map文件,代码中并没有引入这个文件;开发第三方包的时候有用
    'nosources-source-map'	//能看到错误出现的位置,点击错误信息,进去是看不到源代码的,提供了行和列的信息。为了生产环境中保护我们的源代码不会被暴露
]

//yarn webpack 会生成所有模式下的HTML
//serve dist

module.exports = allModes.map(item => {				//对数组单独打包
    return {		
        devtool: item,		//遍历模式的名称
        mode: 'none',		//webpack内部不做处理
        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`
            })
        ]
    }
})

// module.exports = [
// 	{
// 		entry: './src/main.js',
// 		output: {
// 			filename: 'a.js'
// 		}
// 	},
// 	{
// 		entry: './src/main.js',
// 		output: {
// 			filename: 'b.js'
// 		}
// 	}
// ]

选择合适的 Source Map

​ 视频中个人经验 ​ 开发模式 ​ cheap-module-eval-source-map ​ 1、我的代码每行不会超过80个字符 ​ 2、我的代码经过 Loader 转换过后的差异较大 (调整转换前的源代码) ​ 3、首次打包速度慢无所谓,重写打包相对较快 (webpack serve监视模式重新打包,不是每次启动打包) ​ 生产模式 ​ none ​ 1、Source Map 会暴露源代码 ​ 2、调试是开发阶段的事情 ​ 3、对代码没有信心的话:nosources-source-map

没有绝对选择
理解不同模式的差异,适配不同的环境

自动刷新的问题

​ 办法1:代码中写死编辑器的内容 ​ 办法2:额外代码实现刷新前保存,刷新后读取

核心问题:自动刷新导致的页面状态丢失
页面不刷新的前提下,模块也可以及时更新

HMR 介绍

​ Hot Module Replacement ​ 模块热替换,应用运行过程中实时替换摸个模块,应用运行状态不受影响 ​ 热拔插,在一个正在运行的机器上随时插拔设备

自动刷新导致页面状态丢失
热替换只将修改的模块实时替换到应用中

HMR 是 Webpack 中最强大的功能之一
极大程度的提高了开发者的工作效率

开始 HMR

​ 集成在 webpack-dev-serve 中 ​ webpack-dev-serve --hot ​ 也可以通过配置文件开启

devServer: {
    hot: true
    // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
},

const webpack = require('webpack')

new webpack.HotModuleReplacementPlugin()
  • yarn webpack-dev-serve --open css文件可以了,js有些问题

HMR 解惑

​ webpack 中的 HMR 并不可以开箱即用 ​ Webpack 中的 HMR 需要手动处理模块热替换逻辑

1.为什么样式文件的热更新开箱即用
    样式文件已经loader了
2.凭什么样式可以自动处理
    js导出的成员各不相同
3.我的项目没有手动处理,JS照样可以热替换
    你使用了某个框架,框架下的开发,每种文件都是有规律的;通过脚手架创建的项目内部都集成了 HMR 方案

总结:我们需要手动处理 JS 模块更新后的热替换

HMR API

​ devServer: { ​ hot: true ​ },

module.hot.accept('./editor', () => {   //用于注册某一个模块更新后的处理函数;依赖路径,依赖路径后的处理函数;
    console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
})
-   yarn webpack-dev-serve

JS 模块热替换

​ 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 注意事项

​ 1.处理 HRM 的代码报错会导致自动刷新 ​ devServer: { ​ // hot: true ​ hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading ​ },

    -   yarn webpack-dev-serve
2.没启用 HMR 的情况下,HMR API 报错
    if (module.hot) {   }

    热替换关闭,移除插件,yarn webpack,找到打包生成的bundle.js文件,处理热替换被移除掉了

生产环境优化

​ 生产环境和开发环境有很大的差异 ​ 生产环境注重运行效率 ​ 开发环境注重开发效率

模式(mode)

为不同的工作环境创建不同的配置

不同环境下的配置

​ 1.配置文件根据环境不同到处不同配置 ​ 2.一个环境对应一个配置文件

不同环境下的配置文件

​ 1.配置文件根据环境不同到处不同配置 ​ module.exports = (env, argv) => { //env通过cli传递环境名参数 argv运行cli过程中传递的所有参数 ​ if (env === 'production') { //约定生产环境 ​ config.mode = 'production' ​ config.devtool = false //禁用掉source map ​ config.plugins = [ ​ ...config.plugins, ​ new CleanWebpackPlugin(), //开发阶段可以省略的插件 ​ new CopyWebpackPlugin(['public']) //开发阶段可以省略的插件 ​ ] ​ }

        return config
    }
    yarn webpack  开发模式打包

    yarn webpack --env production  会以生产模式进行打包
2.一个环境对应一个配置文件
    大型项目,不同环境对应不同配置文件

    根目录创建webpack.common.js  webpack.dev.js  webpack.prod.js
    - yarn add webpack-merge --dev
    const merge = require('webpack-merge')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    const common = require('./webpack.common')

    module.exports = merge(common, {
        mode: 'production',
        plugins: [
            new CleanWebpackPlugin(),
            new CopyWebpackPlugin(['public'])
        ]
    })

    - yarn webpack --config webpack.prod.js
    可以以生产环境打包应用

DefinePlugin

​ 为代码注入全局成员 ​ process.env.NODE_ENV

plugins: [
    new webpack.DefinePlugin({
        // 值要求的是一个代码片段
        API_BASE_URL: JSON.stringify('https://api.example.com')
    })
]

Tree Shaking

​ [摇掉]代码中未引用部分(dead-code) ​ 比如:console.log()。没有引用到函数 - yarn webpack --mode production 会在生产模式下自动开启

Tree Shaking 使用

​ Tree Shaking 不是指某个配置选项,是一组功能搭配使用后的优化效果,production 模式下自动开启 ​ usedExports 负责标记 [枯树叶] ​ minimize 负责 [摇掉] 他们

optimization: {   //集中配置webpack内部优化的一些功能
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中,   Scope Hoisting
    concatenateModules: true,
    // 压缩输出结果
    // minimize: true
}
- yarn webpack

合并模块函数 Scope Hoisting

​ // 尽可能合并每一个模块到一个函数中, Scope Hoisting ​ concatenateModules: true,

Tree Shaking & Babel

​ Tree Shaking 前提是 ESModules ,由webpack 打包的代码必须使用 ESM ​ 为了转换代码中 ECMAScript 新特性 选择babel-loader 处理js ​ ES Modules -> CommonJS

options: {
    presets: [
        // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
        // ['@babel/preset-env', { modules: 'commonjs' }] //强制使用es babel 插件,转换成mommon js
        // ['@babel/preset-env', { modules: false }]  //不会开启 转换的插件
        // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换。auto根据环境去判断是否开启ES Modules 插件
        ['@babel/preset-env', { modules: 'auto' }]  //配置名称,配置对象
    ]
}

sideEffects 副作用

​ 副作用:模块执行时除了导出成员之外所作的事情 ​ sideEffects 一般用于 npm 包标记是否有副作用

开启
optimization: {
    sideEffects: true,
    // 模块只导出被使用的成员
    // usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    // concatenateModules: true,
    // 压缩输出结果
    // minimize: true,
}

// "sideEffects":false 标识没有副作用

sideEffects 注意

​ 使用前:确保你的代码真的没有副作用

 "sideEffects": [    //标识副作用文件
    "./src/extend.js",
    "*.css"
]

代码分割

​ 所有代码最终都被打包到一起 ​ bundel 体积过大 ​ 并不是每个模块在启动时都是必要的 ​ 分包,按需加载

Code Splitting 分包/代码分割

1.多入口打包       
2.动态导入

多入口打包 Multi Entry

​ 多页应用程序:一个页面对应一个打包入口,公共部分单独提取

entry: {  //定义对象
    index: './src/index.js',
    album: './src/album.js'
},

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']  //形成独立的
    })
]

提取公共模块 Split Chunks

​ 不同入口肯定会有公共模块 ​ optimization: { ​ splitChunks: { ​ // 自动提取所有公共模块到单独 bundle ​ chunks: 'all' ​ } ​ }, ​ 生成 album~index.bundle.js 公共文件

动态导入 Dynamic Lmports

​ 按需加载,需要用到某个模块时,再加载这个模块 ​ 动态导入的模块会被自动分包

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())
​        })
​    }

魔法注释 Magic Comments

​ 灵活处理动态加载的模块,输出时生成的文件 ​ /* webpackChunkName: 'components' */

MiniCssExtractPlugin 提取CSS文件到单独文件中

  • yarn add mini-css-extract-plugin --dev const MiniCssExtractPlugin = require('mini-css-extract-plugin')

    new MiniCssExtractPlugin()

    rules: [ { test: /.css$/, use: [ // 'style-loader', // 将样式通过 style 标签注入 MiniCssExtractPlugin.loader, //考虑是否超过150kb才用 'css-loader' ] } ]

OptimizeCssAssetsWebpackPlugin 压缩CSS

-   yarn add optimize-css-assets-webpack-plugin --dev
-   yarn add terser-webpack-plugin --dev

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

optimization: {
    minimizer: [
    new TerserWebpackPlugin(),  //
    new OptimizeCssAssetsWebpackPlugin()
    ]
},
生产模式运行打包,才会压缩 - yarn webpack --mode production  

输出文件名 Hash substitutions

​ 生产模式下,文件名使用 Hash ​ 1.[name]-[hash].bundel.js 都会发生变化 ​ 2.[name]-[chunkhash].bundel.js 对应文件发生变化,解决缓存问题 ​ 3.[name]-[contenthash].bundel.js 对应文件发生变化 ​ 4.[name]-[xx:8].bundel.js 指定长度

Rollup

​ 更为小巧,仅仅是一款 EXM 打包器 ​ Rollup 中并不支持类似 HMR 这种高级特性 ​ 提供一个充分利用 ESM 各项特性的高效打包器 ​

Rollup快速上手

-   yarn add rollup --dev
    -   yarn rollup
        -   yarn rollup ./src/index.js
            -   yarn rollup ./src/index.js --format iife
                -   yarn rollup ./src/index.js --format iife --filedist/bundle.js

Rollup配置文件

​ 根目录创建 rollup.config.js 文件

    export default {
        input: 'src/index.js',
        output: {
            file: 'dist/bundle.js',   //输出文件名
            format: 'iife'    //输出格式
        }
    }

    -   yarn rollup --config    //声明启用的是config文件
    -   yarn rollup --config rollup.config.js

Rollup使用插件

​ 加载其他类型资源模块 ​ 导入 Common JS 模块 ​ 编译 ECMAScript 新特性 ​ Rollup 支持使用插件的方式扩展,插件是rollup 唯一扩展途径 ​ rollup-plugin-json

    -   yarn add rollup-plugin-json --dev

    import json from 'rollup-plugin-json'

    plugins: [
        json()
    ]

    -   yarn rollup --config

Rollup加载NPM 模块

​ rollup-plugin-node-resolve

-   yarn add rollup-plugin-node-resolve --dev

import resolve from 'rollup-plugin-node-resolve'
resolve()

// 导入模块成员
import _ from 'lodash-es'
log(_.camelCase('hello world'))

-   yarn rollup --config

Rollup Common JS 模块

​ rollup-plugin-commonjs

- yarn add rollup-plugin-commonjs --dev

import commonjs from 'rollup-plugin-commonjs'
commonjs()

//创建cjs-module.js 文件
module.exports = {
    foo: 'bar'
}
//index.js 文件导入和使用
import cjs from './cjs-module'
log(cjs)

Rollup代码拆分

​ Dynamic Imports

//index.js 动态导入
import('./logger').then(({ log }) => {
    log('code splitting~')
})

-   yarn rollup --config --format amd  //覆盖启用amd

dir: 'dist',    //可以输出多个文件
format: 'amd'

-   yarn rollup --config

Rollup多入口打包

​ export default { ​ // input: ['src/index.js', 'src/album.js'], ​ input: { ​ foo: 'src/index.js', ​ bar: 'src/album.js' ​ }, ​ output: { ​ dir: 'dist', ​ format: 'amd' ​ } ​ }

-   yarn rollup --config

<!-- AMD 标准格式的输出 bundle 不能直接引用 -->
<!-- <script src="foo.js"></script> -->
<!-- 需要 Require.js 这样的库 -->
<script src="https://unpkg.com/requirejs@2.3.6/require.js" data-main="foo.js"></script>

选用原则 Rollup/Webpack

优点:

  • 输出结果更加扁平
  • 自动一簇未引用代码
  • 打包结果依然完全可读 缺点:
  • 加载非 ESM 的第三方模块比较复杂
  • 模块最终都被打包到一个函数中,无法实现 HMR
  • 浏览器环境中,代码拆分功能依赖 AMD 库
如果我们正在开发应用程序, 有所欠缺
如果我们正在开发一个框架或者类库,有点有必要,缺点可以忽略
大多数知名框架/库都在使用 Rollup
社区中希望二者并存

总结:webpack 大而全, Rollup 小而美
    引用开发使用 webpack
    库/框架开发使用 Rollup

Parcel 零配置前端应用打包器

  • yarn init

  • yarn add parcel-bundler --dev

  • yarn parcel src/index.html //浏览器自动刷新

    // import $ from 'jquery' //自动安装依赖
    import foo from './foo'
    import './style.css'  //加载第三方
    import logo from './zce.png'
    
    foo.bar()
    
    import('jquery').then($ => {    //动态导入
    $(document.body).append('<h1>Hello Parcel</h1>')
    
    $(document.body).append(`<img src="${logo}" />`)
    })
    
    if (module.hot) {   //支持热替换
    module.hot.accept(() => {   //只可以接收一个参数
        console.log('hmr')
    })
    }
    
  • yarn parcel build src/index.html //以生产模式运行打包

    总结:体验感觉舒服,2017年发布,构建速度更快,完成零配置 webpack 有更好的生态,越来越好用。