require: node 和 ESM 都支持的引入 export / import : 只有ESM 支持的导出引入 module.exports / exports: 只有 node 支持的导出
AMD 异步模块规范通过define函数定义模块通过require函数引入一个模块内部会创建script标签,加载模块代码目前绝大多数三方库都支持AMD CMD(淘宝推出的规范) => sea.js + CMD 使前端代码趋向于CommonJS后被require.js兼容 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。
常用打包工具:
webpackRollupParcel是npm的工具模块,实现了文件的打包功能。对于打包结果,webpack会将文件合并,同时提供基础代码,让打包前后代码之间的逻辑不变。webpack建议根据代码的需要,可以动态引入任何资源,包括css、图片文件等。JS作为前端驱动,资源与JS形成依赖,可以保证上线时的资源文件都是必要的。
loader与plugin是webpack的核心特性。
loader(加载器) 加载不同的资源模块(默认仅支持加载、解析js文件)。loader专注实现资源模块加载。
plugin(插件)
在webpack构建过程的特定时机,注入扩展逻辑,改变或优化构建结果。增强webpack自动化能力,解决除模块加载外的自动化工作。实现大多前端的工程化工作。 webpack 不等于 工程化所有的配置信息入口文件 :webpack.config.js。
除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。默认单位:bitcss文件
css-loader 解析cssstyle-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 } } } ] } }开发时使用一种标准,建议不要把标准混用
webpack支持指定工作模式,分别为:production,development、none。
默认为production模式,对打包结果完全优化,代码会变得不可读。优化方式:TreeShaking、SideEffects。development模式,优化打包速度,增加调试过程中的辅助信息,打包结果适合调试。none模式,最原始的状态打包代码,不进行打包优化。 // 命令行: yarn webpack --mode development // webpack.config.js 中: module.exports = { mode: "development" }webpack 的一种 打包优化,不会打包未使用的代码。 在 webpack 4 中, 生产模式 下会 自动启用,其它模式 下,需要 手动开启 TreeShaking。
// webpack.config.js 中: module.exports = { // 集中配置webpack优化 optimization: { // 只导出外部使用了的成员,负责标记未使用的代码 usedExports:true, // 尽可能将所有模块合并,输出到一个函数中 —————> Scope Hosting(作用域提升) concatenateModules:true, // 开启压缩,负责移除未引用代码 minimize:true } }问题:使用了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新增配置 🌰 : 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" }] ] ] } } ] } }webpack 4新特性,允许使用配置形式来标识代码的副作用,一般在开发npm模块时,才会用到。 工作流程:配置——>标识代码——>是否有副作用—没有副作用—>移除——>TreeShaking获得更大压缩空间
模块除导出成员之外,实现的其它任务。比如原型链扩展。
原理:勾子机制
插件基于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'}) ]browserSync—监听—> dist变化,dist有改变,自动刷新浏览器 watch —监听—> 代码变化,代码有改变,自动重新打包生成dist
集成自动编译和自动刷新浏览器
HMR 即 热更新,集成在webpackDevServer中。实现页面不刷新,也可以看到修改后的效果。
# 命令行 # --open 直接打开默认浏览器 webpack-dev-server --hot --open const webpack = require('webpack') module.exports = { devServer: { hot: true }, plugins: [ new webpack.HotModuleReplacementPlugin() ] }所有代码合并在同一个文件中,可以避免并发请求多个文件,带来的同域并行请求限制、多个请求Header引起的带宽消耗问题。但文件过大时,存在明显缺点:
当前未使用代码也会被大量加载文件过大,加载耗时代码分割实现程序按运行需要进行代码分包,按需加载。提高程序响应速度和运行效果。实现方式:
多入口打包 多页应用程序,一个页面一个打包入口,提取公共部分代码。// 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"] }) ] } 动态导入,按需加载模块 需要用到某个模块时再加载。节省流量。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一个文件就是一个模块)加载代码,并做加载缓存处理,当有缓存中有被加载过的模块,优先读取缓存。
小巧,充分发挥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" />建议开发 应用程序,使用webpack,开发框架或类库,使用Rollup
零配置的前端应用打包器
yarn add parcel-bundler