前端模块化与打包工具

模块化发展史:

  1. 早期通过外部引入文件方式,实现模块化。缺点:污染全局作用域,无法管理模块依赖关系。
  2. 命名空间,文件导出一个全局对象。缺点:无法解决私有空间的问题,模块成员可被修改,无法管理模块依赖关系。
  3. LLFE->立即执行函数,私有变量只能在闭包中访问,需要外部访问的变量,挂载到window上。需要依赖的模块,可以通过闭包做参数声明。

模块化规范:

  1. commonJS
  • 同步加载模块,启动就开始加载模块,运行时只会使用模块。
  • 文件与模块一一对应
  • 每个模块都有单独作用域
  • 通过module.exports 导出成员,require载入模块
  • exports 是 module.export 的别名
    必须要exports.xxx = xxx 导出对象(module.export = xxx 或 module.exports.xxx = xxx)
  • 同步决定了该规范不能用于浏览器,无法处理浏览器的异步需求。

require: node 和 ESM 都支持的引入
export / import : 只有ESM 支持的导出引入
module.exports / exports: 只有 node 支持的导出

  1. AMD
  • 异步模块规范
  • 通过define函数定义模块
  • 通过require函数引入一个模块
  • 内部会创建script标签,加载模块代码
  • 目前绝大多数三方库都支持AMD
  1. CMD(淘宝推出的规范) => sea.js + CMD
  • 使前端代码趋向于CommonJS
  • 后被require.js兼容
  1. ES Modules (ESM)
  • 自动开启严格模式
    文件头不需声明 'use strict'
  • 独立空间
    每一个module都运行在私有作用域中,解决了全局污染问题
  • CORS
    ES Module 通过CORS方式请求外部JS模块
    • 如果响应头不能提供有效的CORS,会产生跨域错误
    • <script type="module" src = "path" /> 仅支持HTTPServer的外部访问,不支持文件形式的访问。
  • 先渲染界面,后加载js
    • 会自动延迟脚本的执行,不阻碍页面渲染,有<script ="true" />的作用。
    • 网页加载script方式:立刻执行。
  • 使用polyfill解决兼容问题
// 以下代码均不能在生产环境中使用,因为动态解析是耗时操作。正确方式为先做好相关编译工作,再打包上线。
// nomodule 关键字,表示script只在不支持module环境下执行
<script nomodule src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
<script nomodule src="https://unpkg.com/browse/promise-polyfill@8.2.0/dist/polyfill.min.js"></script>
// 通过es-module-loader读取代码,然后交给babel转换为兼容版本(eg:ES5)
// promise-polyfill 处理浏览器对promise的兼容问题
  • 前端最主流模块化规范
    Node.js从8.5版本以后,已经在尝试支持ES Module。
# 文件名后缀 .mjs
# 运行命令:
node --experimental-modules demo.mjs
  • ES6才明确的模块化系统
  • 随着webpack等打包工具流行,兼容性也得到改善
  • 导入与导出规则(从语言层,实现了模块化)
    • 可导出成员 export { 成员 },也可导出对象 export default 对象
    • 导入使用import关键字
    • 需要注意:
      • 文件名称要完整
      • 相对路径注意加. ./
      • 绝对路径使用 http:// ... 或者 根目录/.../文件名
      • import './abc.js' 只用于导入
        • import { } from '模块名'{ }不是解构语法
      • import * as object from "./module.js" 所有导出成员,都是object的属性
      • import只能在对顶层,不能嵌套在局部作用域
      • 动态导入:import ('./module.js').then(function(module) { })
      • 常见导出与导入:
        • export default const abc = "123"

        • import 对象, { 成员 } from './module.js'

        • import {default as abc} from './module.js'
          导出对象的另一种写法

        • import 替换为 export, 则成员不能使用,会直接被导出,可用于处理集中导出
          eg : export { default as Test } from './module.js'


目前绝大多数情况下使用的模块化规范

  • ES Modules ——> 前端
  • commonJS ——> 后端

Node.js原生环境,在commonJS中,不能通过require方式,载入ES Module。


ES Modules

<srcipt type="module"></script>

导入与导出

模块化打包工具

常用打包工具:

  • webpack
  • Rollup
  • Parcel

webpack

是什么?

是npm的工具模块,实现了文件的打包功能。对于打包结果,webpack会将文件合并,同时提供基础代码,让打包前后代码之间的逻辑不变webpack建议根据代码的需要,可以动态引入任何资源,包括css、图片文件等。JS作为前端驱动,资源与JS形成依赖,可以保证上线时的资源文件都是必要的。

特性?

  • loader
  • minimizer
  • plugin
  • TreeShaking
  • SideEffects
  • HMR
  • watch工作模式

工作流程?

  • 从入口文件开始,建立依赖树 ——> 递归依赖树——> rules属性 -> test正则匹配文件,使用加载器,加载模块——> loader处理 ——> plugin ——> boundle.js 完成打包

loader与plugin是webpack的核心特性。

loader(加载器)
加载不同的资源模块(默认仅支持加载、解析js文件)。loader专注实现资源模块加载。

plugin(插件)

  • 在webpack构建过程的特定时机,注入扩展逻辑,改变或优化构建结果。
  • 增强webpack自动化能力,解决除模块加载外的自动化工作。
  • 实现大多前端的工程化工作。
    webpack 不等于 工程化

所有的配置信息入口文件 :webpack.config.js。

loader

除js文件外,其他文件都需要在webpack.config.js中声明文件类型对应的loader,转换时loader可以是多个,加载顺序FIFO,最后的loader先加载,但结果一定是标准的JS代码字符串,因为内容最终是拼接到JS文件内。

  • 特性

    • 类管道机制
    • 内不完成一次导入与导出
    • 导出可以交给下一个loader
    • 最终的导出必为JS代码字符串
  • 功能

    • 编译操作
    • 文件转换
    • 代码检查
// 安装loader命令:
yarn add loader名称 --dev

编译操作

由于转换需要,webpack会默认处理ES6的import和export,对于其他ES6特性,需要单独处理

babel
一个处理js代码的平台。处理js代码需要借助babel的插件,babel会将代码转成字符串并生成AST,然后继续转化成新的代码,转换的代码越多,效率就越低。
@babel/preset-env

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

文件转换

  • 图片

    • file-loader
      默认情况下,生成的文件的文件名就是文件内容的 MD5 哈希值,并会保留所引用资源的原始扩展名。
    • url-loader
      指定options-limit,限制url-loader转换图片的大小,超过限制的图片会直接使用file-loader转换,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。默认单位:bit
  • css文件

    • css-loader
      解析css
    • style-loader
      加载css-loader生成的样式,以style标签形式注入到页面中。
    module.exports = {
    // 有三种类型
    mode: 'none',
    entry: '入口path,相对路径',
    output: {
        filename:'文件名',
        path: path.join(__dirname, 'dist'),
        pubilcPath: '根目录/'
    }
    module: {
        rules: [
            {
                test: /.js$/,
                use: {
                    // 处理es6新特性
                    loader: "babel-loader",
                    options: {
                        presets: ["@babel/preset-env"]
                    }
                }
            },
            {
                test: /.css$/,
                // 注意:当有多个loader时,webpack执行顺序是由后往前(队列)
                use:[
                    // 将css转换结果,通过style标签形式,注入到页面
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use:{
                    'url-loader',
                    options: {
                        // 超过这个大小,webpack会自动寻找file-loader
                        // 没有安装file-loader,会报错
                        limit: 10 * 1024 
                    }
                }   
            }
        ]
      }
    }
    

兼容多种标准

  • 遵循ES Modules 标准的import 声明
  • 遵循commonJS 标准的 require 函数、exports.xxx 导出
  • 遵循AMD 标准的define、require 函数

开发时使用一种标准,建议不要把标准混用

打包优化

webpack支持指定工作模式,分别为:production,development、none。

  • 默认为production模式,对打包结果完全优化,代码会变得不可读。优化方式:TreeShaking、SideEffects。
  • development模式,优化打包速度,增加调试过程中的辅助信息,打包结果适合调试。
  • none模式,最原始的状态打包代码,不进行打包优化。
// 命令行:
yarn webpack --mode development

// webpack.config.js 中:
module.exports = {
    mode: "development"
}

TreeShaking

webpack 的一种 打包优化不会打包未使用的代码
webpack 4 中, 生产模式 下会 自动启用其它模式 下,需要 手动开启 TreeShaking。

// webpack.config.js 中:
module.exports = {
// 集中配置webpack优化
optimization: {
    // 只导出外部使用了的成员,负责标记未使用的代码
    usedExports:true,
    // 尽可能将所有模块合并,输出到一个函数中 —————> Scope Hosting(作用域提升)
    concatenateModules:true,
    // 开启压缩,负责移除未引用代码
    minimize:true
    }
}

treeShaking 与 Babel

问题:使用了babel,treeShaking一定会失效吗?

解析: 原理与源码探究:

  • 原理
    • 使用treeShaking 前提,代码 必须 遵循ES Module 编写
    • babel-loader 用来转换代码的新特性
    • 转换过程中,有将 ES Module ——转换(取决于插件)——> Common JS的可能
  • 源码

最新的Babel已经开启ES Module的支持,在node_modules->babel->lib->injectCaller.js中查阅

在这里插入图片描述

preset-env根据Babel的设置,也自动禁用了对ESM的转换

preset-env
  • 为preset-env新增配置
    🌰 :
module.exports = {
    mode: 'none',
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    // 缓存已经编译过的文件
                    // loader: 'babel-loader?cacheDirectory=true',
                    // 或者
                    // loader: 'babel-loader',
                    // options: {
                    //     cacheDirectory: true
                    // }]
                    
                    // 只在src文件夹下查找
                    // include: [resolve('src')],
                    
                    // 不去查找的文件夹路径,node_modules下的代码是编译过得,没必要再去处理一遍
                    // exclude: /node_modules/,
                    options: [
                        presets: [
                            ['@babel/preset-dev', {
                                // 新增配置
                                // 不会开启ES Module转换的配置
                                modules:false
                                // 转换为commonjs
                                // modules: "commonjs"
                            }]
                        ]
                    ]
                }
            }
        ]
    }
}

SideEffects

webpack 4新特性,允许使用配置形式来标识代码的副作用,一般在开发npm模块时,才会用到。
工作流程:配置——>标识代码——>是否有副作用—没有副作用—>移除——>TreeShaking获得更大压缩空间

什么是副作用?

模块导出成员之外,实现的其它任务。比如原型链扩展。

副作用的配置?

  • webpack.config.js
    用于表示,当前项目目录 是否启用 副作用,true表示启用
  • package.json
    用于表示,当前package.json所影响的项目 是否有 副作用,false表示没有副作用
// webpack.config.js
optimization: {
 sideEffects:true
}
// package.json
"sideEffects": false

plugin

原理:勾子机制

插件基于webpack在工作环节留下的钩子,为webpack扩展功能。插件本质:函数 或 包含apply方法的对象。

class MyPlugin {
    // webpack自动调用
    apply (compiler) {
        // compiler核心参数,包含注册信息
    }
}
plugins: [
    // 自定义plugin时,要手动添加UglifyJsPlugin,否则js代码不会压缩
    // 可以用ParallelUglifyPlugin代替自带的 UglifyJsPlugin插件,实现单线程到并行打包
    new ParallelUglifyPlugin({
      cacheDir: '.cache/',
      uglifyJS:{
        output: {
          comments: false
        },
        compress: {
          warnings: false
        }
      }
    }),
    // new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: './src/demo.html'})
  ]

常用plugin

  • clean-webpack-plugin
    清除dist目录内容。
  • html-webpack-plugin
    用于导出html文件,没创建一个html-webpack-plugin对象,可导出一个html文件。可实现多页面导出。
  • copy-webpack-plugin
    将文件拷贝到dist目录(上线时用)。
  • optimize-css-assets-webpack-plugin
    压缩css文件

自动刷新

watch 和 browserSync

browserSync—监听—> dist变化,dist有改变,自动刷新浏览器
watch —监听—> 代码变化,代码有改变,自动重新打包生成dist

webpackDevServer

集成自动编译和自动刷新浏览器

HMR

HMR 即 热更新,集成在webpackDevServer中。实现页面不刷新,也可以看到修改后的效果。

# 命令行
# --open 直接打开默认浏览器
webpack-dev-server --hot --open

const webpack = require('webpack')

module.exports = {
devServer: {
    hot: true
},
plugins: [
    new webpack.HotModuleReplacementPlugin()
]

}

代码分割(code splitting)

所有代码合并在同一个文件中,可以避免并发请求多个文件,带来的同域并行请求限制、多个请求Header引起的带宽消耗问题。但文件过大时,存在明显缺点:

  1. 当前未使用代码也会被大量加载
  2. 文件过大,加载耗时

代码分割实现程序按运行需要进行代码分包,按需加载。提高程序响应速度和运行效果。实现方式:

  1. 多入口打包
    多页应用程序,一个页面一个打包入口,提取公共部分代码。
    // webpack.config.js
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    module.exports = {
        entry: {
            demo: "./demo.js",
            test: "./test.js"
        },
        output: {
            // 项目级别hash
            // filename: "[name]-[hash].bundle.js"
            // 根据内容,不同文件有独立的hash值
            // filename: "[name]-[contenthash].bundle.js"
            // (推荐)同一chunk下的文件hash值相同, :8表示输出8位hash值
            filename: "[name]-[chunkhash:8].bundle.js"
        },
        optimization: {
            splitChunks: {
                // 提取复用代码公共的bundle中
                chunks: "all"
            }
        },
        plugins: [
            new HtmlWebpackPlugin({
                title: "nuti entry",
                template: "./demo.html",
                filename: "demo.html",
                // 只加载各自bundle的配置
                chunks: ["demo"]
            }),
            new HtmlWebpackPlugin({
                title: "nuti entry",
                template: "./test.html"
                filename: "test.html",
                chunks: ["test"]
            })
        ]
    }
    
  2. 动态导入,按需加载模块
  • 需要用到某个模块时再加载。节省流量。
  • webpack支持动态导入方式实现按需加载模块,动态导入的模块会被自动分包。
  • 动态加载方式产生的文件,名称是一个序号,生产模式下也不需要关心文件名。需要添加名称时,可以使用webpack的魔法注释

魔法注释说明:相同注释名称的组件,会被合并到同一文件。

  • css模块的按需加载插件:MiniCssExtractPlugin,可提取css到单文件中。
    • 建议当样式文件大小超过150KB时使用 MiniCssExtractPlugin
    yarn add mini-css-extract-plugin --dev
    
    // webpack.config.js
    const HappyPack = require('happypack')
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    const terserWebpackPlugin = require('terser-webpack-plugin')
    module.exports = {
        optimization: {
            // 自定义minimizer时,webpack默认js压缩会关闭
            minimizer: [
                // 压缩css文件,只会在minimize特性开启时有作用,生产模式自动开启minimize
                new OptimizeCssAssetsWebpackPlugin(),
                // 重新开启js压缩, 安装yarn add terser-webpack-plugin --dev
                new terserWebpackPlugin()
            ]
        },
        module: {
            rules: [
                test: /.css$/,
                use: [
                    // 替换 style-loader,MiniCssExtractPlugin采用link方式,从外部引入css样式
                    MiniCssExtractPlugin.loader,
                    "css-loader"
                ]
            ]
        },
        plugins: [
            new MiniCssExtractPlugin(),
            // 任何模式都会压缩css
            // new OptimizeCssAssetsWebpackPlugin()
            new HappyPack({
              // id标识happypack处理那一类文件
              id: 'happyBabel',
              // 配置loader
              loaders: [{
                loader: 'babel-loader?cacheDirectory=true'
                // loaders: ['babel-loader?cacheDirectory'],
              }],
              // 共享进程池
              threadPool: happyThreadPool,
              threads: 4, // 线程开启数
              // 日志输出
              verbose: true
            })
        ]
    }
    
import demo from './demo.html'
import test from './test.html'
const render = () => {
    const mainElement = document.querySelector('.main')
    mainElement.innerHTML = ''
    if (hash === '#demo') {
        
        document.body.appendChild(demo())
        // 魔法注释例子:/* webpackChunkName: 名称 */
        import (/* webpackChunkName: demo */'./demo.js').then(function({default: demo}) { 
            document.body.appendChild(demo)
        })
    }
    else if (hash === '#test') {

        document.body.appendChild(test())
        import ('./test.js').then(function({default: test}) { 
            document.body.appendChild(test)
        })
    } 
}

在React 或 Vue 项目中的路由组件,可通过动态加载方式,按需加载组件

其它

webpack打包后的代码,是自调用函数,运行时,函数开始自调用,会一个一个的按模块(前面提到过,ESM一个文件就是一个模块)加载代码,并做加载缓存处理,当有缓存中有被加载过的模块,优先读取缓存。

Rollup

小巧,充分发挥ESM特性的一个ESM打包器,相比webpack,打包结果更简洁,默认会开启TreeShaking(Rollup率先提出TreeShaking概念),优化打包结果。

配置(入口)文件

rollup.config.js,可以使用ESM规范编写。

// import resolve from 'rollup-plugin-node-resolve'
export default {
    // 多入口打包时input设置,foramt:amd,Rollup会自动处理公共模块合并到一个文件中
    //input: ["src/index.js","src/test.js"],
    // 或
    //input: {index: "src/index.js",test: "src/test.js"},
    // 单入口导入
    input: "src/index.js",
    output: {
        // 指定输出文件夹,用于处理多文件输出
        // dir:"dist",
        // 指定输出格式amd,可用于处理分包(代码拆分), 使用 import().then({ 成员 }) => { } 实现代码拆分
        // format: "amd"
        // 指定输出文件
        file: "dist/bundle.js",
        // 指定输出格式,iife=>自执行函数,本身没有分包机制(webpack的iife有基础代码处理分包),所有文件都会打包进同一个文件
        format: "iife"
    },
    plugins: [
        // 插件
        // resolve()
    ]
}

扩展

唯一方式:使用插件机制。默认仅可加载 ES Module 模块。

  • 加载npm模块需要安装插件:rollup-plugin-node-resolve
  • 加载commonJS模块需要安装插件:rollup-plugin-commonjs

多入口文件加载

需要安装require.js

<script src="./require.js" data-main="index.js" />

优缺点

优点:

  • 输出结果更加扁平
  • 自动移除未引用代码
  • 打包结果依然完全可读

缺点:

  • 加载非ESM的三方模块较为复杂
  • 模块最终都被打包到一个函数中,无法实现HMR
  • 浏览器环境,代码拆分需要依赖AMD库

建议开发 应用程序,使用webpack,开发框架或类库,使用Rollup

Parcel

零配置的前端应用打包器

yarn add parcel-bundler
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350