模块化:对代码按照功能的不同去划分不同的模块
模块化演变过程
早期:
- 文件划分方式
- 污染全局作用域
- 命名冲突问题
- 无法管理模块依赖关系
- 命名空间方式
- IIFE(立即执行函数)
模块化规范
构成:模块化标准 + 模块加载器
- CommonJS规范:以同步模式加载模块,启动时加载,执行不用加载(缺点)
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数载入模块
编译后的文件名如果是
vue.runtime.js,模块化采用的是CommonJS
- 为浏览器设计新规范:AMD(Asynchronous Module Definition)
require([./module1], function(module1){
module1.start()
})
- 使用复杂
- 模块js文件请求频繁
- Sea.js + CMD
define(function (require, exports, module){
var $ = require('jquery')
module.exports = function () {
console.log('hi')
$('body').append('<p>hi</p>')
}
})
ES Modules + CommonJS:模块化最佳实践方式
Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:
module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。
ES Modules
基本特性
通过script添加 type = "module" 的属性,就可以以 ES Module 的标准执行其中的JS代码。使用:
<script type = "module">
console.log('hi')
</script>
serve .//npm工具去启动
-
ESM自动采用严格模式,忽略
'use strict'- this不指向全局对象
-
每个ES Module都是运行在单独的私有作用域中
-
ESM是通过 CORS 的方式请求外部 JS 模块的
- 如果src标头不支持 CORS,会报跨域的错误
-
ESM 的 script标签会延迟执行脚本
- 脚本写在标签之前,脚本完成之后才会显示对应的标签
导入 / 导出
export / import
//./module.js
const foo = 'es modules'
export {foo as fooName}//重命名
export {foo as default}
//./app.js
import { fooName } from './module.js'
import { defaul tas fooName } from './module.js'//必须要指定变量名
console.log(fooName)
- 导出:
-
导出不是字面量
如果要导出一个字面量:
export default {name,age} -
暴露出来的是一个引用关系,只读
- **导出:**导出时文件路径要填写完整
-
导入一些不需要外界控制的子模块
import {} from './module.js' import './module.js' -
把所有导出成员都导出来
import * from './module.js' -
动态导入模块机制:import...from...只能出现在最顶层
import('./module.js')//返回的是一个promise,promise是异步加载 .then(function(module){ console.log('hi') }) -
提取模块中默认成员写法
import title from './module.js' //等价于import {default as title} from './module.js'
-
导出导入成员:如果将import改成export,那么这些成员将作为目前模块的导出成员看待;同时,目前模块也将无法访问到该导出成员
//import { foo } from './module.js' export { foo } from './module.js'实例:当需要导入的模块过多时,可以新建一个中转文件index.js,把组件导入再导出;default参数必须要进行重命名
//./index.js export { Button } from './button.js' export { Avatar } from './avator.js' export { default as newB } from './button.js'
ESM浏览器环境Polyfill
IE不兼容ESM标签:引用browser-es-module-loader去使用ESM
-
引入脚本文件到ESM中:cdn服务 unpkg.com/browser-es-module-loader 拿到js文件
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script> <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script> <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>- 因为在支持的浏览器中,browser-es-module-loader会重复加载,使用 nomodule 属性使其只在不支持的浏览器中工作
- 在生产阶段不要这样用,动态解析脚本会使工作效率变差
ESM运行原理:
- 将浏览器不识别的ESM交给babel转换
- 需要import的文件通过ajx请求回来的代码再通过babel转换
ESM in node.js(过渡状态)
- 支持情况
- 使生产阶段也可以在不同浏览器使用ESM
使用:
1. .js => .mjs
2. node --experimental-modules index.js
不支持第三方模块导出默认成员,因为第三方模块并没有向外去暴露出一个成员/第三方模块都是导出默认成员
import { camelCase } from 'lodash'
//错误的写法
-
与CommonJS交互
-
ESM可以导入CJS模块
-
CJS不能导入ESM模块
-
CJS始终只会导出一个默认成员
import { foo } from './common.js' //foo无法提取 -
注意:import不是解构导出对象(只是一个固定的用法
-
-
与CommonJS模块的差异
//cjs //加载模块函数 console.log(require) //模块对象 console.log(module) //导出对象别名 console.log(exports) //当前文件的绝对路径 console.log(__filename) //当前文件所在目录 console.log(__dirname)- ESM中没有CJS的那些模块全局成员了
新版本支持
//package.json
{
"type":"module"
}
xx.mjs => xx.js
common.js => common.cjs
常用的模块化打包工具 / Webpack打包
由来 / 概要
几种模块化对生产环境产生的影响:
- ESM存在环境兼容问题
- 模块文件过多,网络请求频繁
- 所有前端资源都需要模块化
功能:
- 新特性代码编译
- 模块化JavaScript打包
- 支持不同类型的资源模块
概要
模块打包器(module bundler):将零散的代码打包到JS文件中
模块加载器(loader):将兼容有问题的代码编译转换
代码拆分(code splitting):挑选有需要的代码打包。当实际中需要到某个模块,再通过异步去加载它,实现增量加载或者渐进式加载
资源模块(asset module):webpack支持以模块化载入任意的资源文件,例如,JavaScript中可以支持import CSS文件
模块化工具的作用:打包工具解决的是前端整体的模块化,并不单指JavaScript模块化
上手
yarn init --yes
yarn add webpack webpack-cil --dev
//引入webpack模块
yarn webpack
//webpack从index.js开始打包
//打包结果会存放在dist目录里
//可以把webpack命令放在package.json中简化打包过程
"scripts":{
"build":"webpack"
}
yarn build
配置文件
约定:入口文件src/index.js -> dist/main.js
自定义:src/main.js
-
在根目录下添加webpack.config.js
const path = require('path') module.exports = { entry:'./src/main.js', output:{ filename:'bundle.js', //path:'output' path:path.join(__dirname,'output') //指定输出文件所在的目录 //都需要使用绝对路径 } }- __dirname: 总是返回被执行的 js 所在文件夹的绝对路径
- __filename: 总是返回被执行的 js 的绝对路径
- process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径
- ./: 跟 process.cwd() 一样,返回 node 命令时所在的文件夹的绝对路径
工作模式
yarn webpack
//默认使用prouction模式工作:webpack会启动一些优化插件,自动压缩代码
yarn webpack --mode development
//添加一些调试过程的辅助到代码当中
yarn webpack --mode none
//原始状态打包
也可以在配置中添加此属性
//webpack.config.js
const path = require('path')
module.exports = {
mode:'development',
...
}
打包结果运行原理
模块私有作用域
webpack资源模块加载
- Loader是Webpack的核心特性,借助不同的Loader可以加载任何类型的资源
默认不解析css文件,需要用适当的加载器loader去处理此类型资源文件,使得css文件转换成一个js模块
-
安装css-loader
yarn add css-loader --dev yarn add style-loader --dev//将转换的js结果通过style标签追加到页面上 -
添加加载资源规则配置
//webpack.config.js module: { rules:[ { test:/.css$/, use:[ 'style-loader', 'css-loader' //从后往前执行 ] } ] }
webpack导入资源模块
-
打包入口 ≈ 运行入口
-
JavaScript驱动整个前端应用的业务
webpack建议在js引入css文件,原因:
-
根据代码的需要动态导入资源
-
需要资源的不是应用,而是代码
import './xxx.css'
意义:
- 逻辑合理,js确实需要这些资源文件
- 确保上限资源不缺失,都是必要的
文件资源加载器(file-loader)
示例:
- 需要转换成js文件
import icon from './icon.png'
const img = new Image()
img.src = icon
document.body.append(img)//到body中
//webpack.config.js
module: {
rules:[
...
{
test:/.png$/,
use:'file-loader'
}
]
}
yarn webpack
- 配置最终网站文件在根目录的位置,才能正常显示网站图片
//webpack.config.js
output:{
...
publicPath:'dist/'
}
Data URLs 与 url-loader
-
Data URLs:以代码的形式直接表示一个文件
-
不需要独立的物理文件了
-
合适打包体积小的资源,体积过大打包文件就过大,运行时间长
使用:
-
安装url-loader
yarn add url-loader --dev -
配置
//webpack.config.js { test:'/.png$/', use:{ loader:'url-loader', options:{ limit: 10 * 1024 //只处理10kb以下的文件!还是要安装file-loader模块 } } }
优化:
小文件使用Data URLs,减少请求次数
大文件单独提取,提高加载速度
常用加载器分类
编译转换类:css-loder以JS形式工作的css模块文件操作类:flie-loader导出文件访问路径代码检查类:eslint-loader检查通过/不通过
webpack编译ES6
!webpack不会自动编译ES6,因为模块打包需要,才会处理import和export
- webpack只是打包工具
- 加载器可以用来编译转换代码
-
使用babel-loader编译代码
yarn add babel-loader @babel/core @babel/preset-env --dev//webpack.config.js rules: [ { test: /.js$/, use: { loader:'babel-loader', options:{ presets:['@babel/preset-env'] //env插件集合,包括了全部的ES特性 } } } ]
模块加载方式
webpack兼容多种模块化标准:
-
遵循ES Modules标准的import声明
import './xxx.css' -
遵循CommonJS标准的require函数
const createHeading = require('./xx.js').default -
遵循AMD标准的define函数和require函数
define(['./heading.js','./main.css'], (createHeading, icon) => { ... }) require(['./heading.js','./main.css'], (createHeading, icon) => { ... })
- 以上最好不要混合用
- loader加载的非JavaScript也会触发资源加载,样式代码中的
@import指令和url函数 - HTML代码中图片标签的src属性
所有需要引用资源的地方都会被webpack找出来,根据不同的配置交给不同的loader处理,最后将处理的结果整体打包到输出目录
webpack就这样完成项目的模块化
核心工作原理
-
依赖树
webpack递归此依赖树,找到对应节点资源文件,根据rule使用加载器加载这个模块,最后的结果放到bundle.js中
-
loader机制是webpack的核心
开发一个loader
- loader负责资源文件从输入到输出的转换
- loader ≈ 管道
- 对于同一个资源可以依次使用多个loader
示例:css-loader → style-loader
webpack插件机制 / plugin
-
loader专注实现资源模块加载
-
plugin解决其他自动化工作:清除dist目录、拷贝静态文件至输出目录、压缩输出代码
自动清除输出目录插件 / clean-webpack-plugin
yarn add clean-webpack-plugin --dev
//webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugin: [
new CleanWebpackPlugin()
]
每次执行前就可以清除dist目录的文件了
自动生成html插件 / html-webpack-plugin
根目录里有index.html文件,每次部署都需要上次dist文件和index.html,我们可以直接使用webpack打包html文件,删除根目录文件
- 使用插件
- 自定义输出内容
- 同时输出多个页面文件
自动生成使用bundle.js的html
yarn add html-webpack-plugin --dev
//webpack.config.js
const htmlWebpackPlugin = require('html-webpack-plugin')
//不需要解构
output: {
filename:'bundle.js',
path: path.join(__dirname, 'dist')
//publicPath: 'dist/'
}
plugin: [
//用于生成index.html
new htmlWebpackPlugin({
title:'Webpack Plugin Sample',
//html标题
meta:{
viewport:'width=device-width'
//对象的形式设置页面元素标签
}
/**
* 自定义输出内容
*/
}),
//用于生成about.html
new HtmlWebpackPlugin({
filename:'about.html',
})
]
yarn webpack
最后会输出一个index.html文件到打包目录
静态文件拷贝 / copy-webpack-plugin
将public里的文件完整拷贝到输出目录
yarn add copy-webpack-plugin --dev
//webpack.config.js
const copyWebpackPlugin = require('copy-webpack-plugin')
//不需要解构
plugin: [
...
new copyWebpackPlugin([
'public'
])
]
yarn webpack
loader只加载模块,plugin拥有更宽的能力范围
插件机制的工作原理
-
plugin通过
钩子机制实现钩子类似于web中的事件,为了便于插件扩展,webpack给每一个环节都埋下了钩子Honk。开发插件时,往不同的节点挂载不同的任务,就可以扩展webpack的能力
-
webpack要求插件必须是一个函数或者一个包含
apply方法的对象////webpack.config.js class MyPlugin { /** * compiler对象包含了此次所有的配置信息 */ apply(compiler) { console.log('MyPlugin启动时调用') //名称1 + 挂载到钩子的函数2 compiler.hooks.emit.tap('MyPlugin', compilation => { //compilation此次打包的上下文 for (const name in compilation.assets){ /** * name 文件名 * compilation.assets[name].source() 文件内容 */ if(name.endsWith('.js')){ //判断文件名是否以.js结尾 const contents = compilation.assets[name].source() const withoutComments = contents.replace(/\/\*\*+\*\//g,'')//全局替换注释 } compilation.assets[name] = { source: () => withoutComments, size: () => withoutComments.length } } }) } } //-----使用------ plugins:[ new MyPlugin() ]
eg:清除bundle.js中不必要的注释
借用emit()钩子
实现:通过在生命周期的钩子中挂载函数实现扩展
理想的开发环境
- 以HTTP Server运行
- 自动编译 + 自动刷新
- 提供Source Map支持
自动编译
-
watch工作模式:监听文件变化,自动重新打包yarn webpack --watch
自动刷新浏览器
-
BrowserSync自动刷新功能browser-sync dist --files "**/*" -
缺:麻烦、效率低
Webpack Dev Server
-
Webpack Dev Server提供用于开发的HTTP Server:集成自动编译和自动刷新浏览器yarn add webpack-dev-server特点:为了工作效率没有将结果打包到磁盘中,减少磁盘的读写操作
-
静态文件:开发阶段不需要参与webpack构建,但同样需要被served
-
默认只会serve打包输出文件
-
只要是webpack输出的文件,都可以直接被访问
-
其它资源文件也需要serve
//webpack.config.js module.exports = { ... devServer: { contentBase: './public' //可以额外为开发服务器指定查找资源目录 } } plugins: [ //开发阶段最好不要使用这个插件:静态文件拷贝 //new CopyWebpackPlugin(['public']) ] -
代理API
-
解决开发环境接口跨域请求问题
域名协议端口不一致
-
解决方式1:
跨域资源共享(CORS),使用的CORS的前提是API必须支持,并不是任何情况下API都应该支持。如果同源部署就没有必要使用CORS -
解决方式2:
Webpack Dev Server支持配置代理-
目标:将github API代理到开发服务器
devServer: { proxy:{ '/api':{ //http://localhost:8080/api/users 相当于请求 http://api.github.com/api/users target:'http://api.github.com',//代理目标 //http://localhost:8080/api/users 相当于请求 http://api.github.com/users pathRewrite: { '^/api':'' }, //不能使用localhost:8080作为请求github的主机名 changeOrigin: true } } }主机名是http协议中的相关概念
-
Source Map / 源代码地图
解决:运行代码与源代码之间完全不同,无法调试应用,错误信息无法定位。调试和报错都是基于运行代码
-
Source Map是一个独立的map文件,与源码在同一个目录下Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置
-
运行代码通过Source Map逆向解析得到源代码
例子:新版jquery.min.js文件手动添加注释
//# sourceMappingURL = jquery-3.4.1.min.map -
Source Map解决了源代码与运行代码不一致所产生的我问题
webpack配置Source Map
基本使用:
//webpack.config.js
devtool:'source-map'
bundle.js文件最后也自动加上注释
yarn webpack
sever dist
- webpack支持12种不同的方式,每种方式的效率和效果各不同
eval模式下的Source map
eval是js中的一个函数
控制台中的eval运行在虚拟机中,可以使用
sourcelURL声明这段代码所属的文件路径
设置成eval模式:
//webpack.config.js
devtool:'eval'
- 浏览器知道这段代码所对应的文件,从而实现错误定位的文件,这种模式下不会生成source map文件,构建速度最快,但效果简单
- 只能定位源代码的名称而不能定位行列和信息
webpack devtool模式对比
eval:只能定位哪一个文件除了错误
eval-source-map:同样使用eval模式执行代码,可以定位到行和列的信息。相比eval生成了source-map
cheap-eval-source-map:只能定位到行信息,快一点,显示ES6转换后的代码
cheap-module-eval-source-map:也定位到行,但显示的是我们写的源代码没有经过loader加工
inline-source-map:普遍的source-map文件都是以物理文件存在,而该模式是使用dataURL的方式嵌入到代码中。代码体积大很多,不常用
hidden-source-map:构建当中生成了该文件但不展示
nosources-source-map:没有源代码但是提供了行列信息。以上两者保护生产环境代码不被暴露
eval是否使用eval执行模块代码
cheap-source-map是否包含行信息
module是否能够得到loader处理之前的源代码
如何选择Source Map模式(个人)
-
开发模式:cheap-module-eval-source-map
-
生产模式:不生成source-map
-
调式阶段:nosources-source-map,定位到源代码位置,不至于向外暴露你源代码的内容
模块热替换 HMR
自动刷新:不丢失当前页面状态
HMR集成在webpack-dev-server中
webpack-dev-server --hot
或者在配置文件中开启
//webpack.config.js
const webpack = require('webpack')
const HtmlWebpack = require('html-webpack-plugin')
//载入webpack内置插件
devServer: {
hot: true
}
plugin: [
new webpack.HotModuleReplacementPlugin()
]
yarn webpack-dev-server --open
特点
- 不可以开箱即用:需要手动处理模块热替换逻辑
- 样式文件的热更新开箱即用:样式文件经过loader处理的
- 使用了某个框架可以自动热更新,框架下的开发,每种文件都有规律
- 通过脚手架创建的项目都集成了HMR方案
- 我们需要手动处理JS模块更新后的热替换
HMR APIs
- 为JS提供了一套处理HMR的API,处理当某一个模块更新后该如何替换到页面
//main.js
//如果对这个模块进行处理了就不会自动刷新
module.hot.accept('./editor', () => {
//editor更新需要在这里手动处理热替换逻辑
})
webpack处理js模块热替换
eg:针对editor模块处理热替换
//main.js
let lastEditor = editor
module.hot.accept('./editor', () => {
const value = lastEditor.innerHTML
//先保存状态
document.body.removeChild(editor)
//移除元素
const newEditor = createEditor()//创建一个新的元素追加到页面当中
newEditor.innerHTML = value
//再设置新元素状态
document.body.appendChild(newEditor)
})
更加通用的热替换:图片模块热替换
//main.js
//注册这个图片的热替换处理函数
const img = new Image()
module.hot.accept('./better.png', () => {
img.src = background
//设置为新的src就可以了
})
- 要写一些额外的代码但是利大于弊
HMR注意事项
-
处理HMR的代码报错会导致自动刷新:使用
hotOnly -
没启用HMR的情况下,HMR API报错
因为没有
module.hot这个对象if(module.hot){ ...业务代码 } -
代码中多了一些与业务无关的代码,但不会影响生产环境
生产环境优化
生产环境注重运行效率——模式(mode)
为不同的工作环境创建不同的配置:
-
配置文件根据环境不同导出不同配置
webpack支持导出一个函数,这个函数返回一个配置对象
//webpack.config.js //env-通过cli传递的环境名参数,argv-运行cli传递的所有参数 module.exports = (env, argv) => { const config = { //开发模式 mode:'development', ... } if(env === 'production'){ config.mode = 'production' config.devtool = false//禁用掉source map config.plugins = [ ...config.plugin, new CleanWebpacPlugin() new CopyWebpackplugin(['public']) //开发阶段的插件,只有在上限打包之前才有自己的价值 return config ] } }yarn webpack --env production -
不同环境对应不同配置文件
公共配置:
//webpack.common.js
//直接把webpack.dev.js复制过来
生产环境配置:
//webpack.prod.js
const common = require('./webpack.common')
//导入公共配置
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
//把公共对象复制到其中,并且可以覆盖掉公共模块中的一些配置
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin()
new CopyWebpackPlugin(['public'])
]
})
//需要合并webpack配置的需求
yarn add webpack-merge --dev
运行webpack的时候需要--config添加指定的文件名
yarn webpack --config webpack.prod.js
命令也可以定义到package.json中
//package.json
"scripts": {
"build": "webpack --config webpack.prod.js"
}
yarn build
Webpack DefinePlugin
-
为代码注入全局成员
process.env.NODE_ENV //第三方模块都是针对这个成员去判断当前的运行环境,决定是否去执行打印日志等操作
//webpack.config.js
const webpack = require('webpack')
module.exports = {
plugin: [
new webpack.DefinePlugin({
//API_BASE_URL: '"http://api.example.com"'
//为代码注入一个API服务地址
API_BASE_URL: JSON.stringify('http://api.example.com')
//可以先转换成表示这个值的代码片段
})
//此对象每一个键值都会注入到代码中
]
}
//main.js
console.log(API_BASE_URL)//http://api.example.com
Tree-shaking
-
摇掉代码中未引用的部分 /
未引用代码(dead-code) -
比如一些console.log语句
yarn webpack --mode production冗余代码并没有输出
Tree-shaking不是指某个配置选项,是一组功能搭配使用后的优化效果
production模式下自动开启
在其它模式开启的办法:
//webpack.config.js module.exports = { mode: 'none', ... optimization: { usedExports: true, //在导出里只导出外部使用的成员 - 负责标记”枯树叶“ minimize: true //未引用的代码都被移除掉了 - 负责摇掉 } }
webpack合并模块 / Scope Hoisting
-
作用域提升 -
尽可能将所有模块合并输出到一个函数中
-
既提升了运行效率,又减少了代码体积
Tree Shaking与Babel
-
Tree Shaking前提是ES Modules
-
由webpack打包的代码必须使用ESM
-
为了转换代码中的ECMAScirpt新特性,会使用
babel-loader处理JS,将ESM 转换为 CommonJS//webpack.config.js module: { rules: [ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [['@babel/preset-env', {modules: 'commonjs'}]] //强制配置babel } } ] }
## sideEffects
- 确保你的代码没有副作用
```json
//webpack.json
"sideEffects": [
'./src/extend.js',
'*.css'
]
- bundle.js里有副作用的模块都被打包进来了
code-splitting / 代码分包-代码分割
- 所有代码最终都被打包到一起,造成bundle体积过大
- 但并不是每个模块在启动时都是必要的
分包,按需加载
按照不同的规则打包到不同的bundle,从而提高应用的响应速度
多入口打包
-
适用于多页应用程序,一个页面对应一个打包入口,公共部分单独提取
-
配置
entry//webpack.config.js entry: { index: './src/index.js', album: './src/album.js' } //对象中,一个属性就是一个打包入口 output: { filename: '[name].bundle.js' } //动态替换成入口文件名 plugin: [ new HtmlWebpackPlugin({ ... filename: 'index.html' chunks:['index'] }) new HtmlWebpackPlugin({ ... filename: 'index.html', chunks:['album'] }) ] //为两个页面配置两个不同的chunk我们期待一个页面使用一个对应结果,配置chunk之后会输出两个页面index.html / album.html
yarn webpack -
问题: 不同入口中肯定有公共模块被import
提取公共模块
//webpack.config.js
optimization: {
splitChunks: {
chunks: 'all'
}
}
yarn webpack
会生成两个相同模块的公共部分album~index.html.js
动态导入
-
按需加载,需要用到某个模块时,再加载这个模块
-
动态导入的模块会被
自动分包//index.js if(hash === '#posts'){ import('./posts/posts').then(({default: posts}) => { mainElement.appendChild(posts()) //创建界面上的元素 }) }else if(hash === '#album') { import('./album/album').then(({default:album}) => { mainElement.appendChild(album()) }) }
魔法注释
-
给bundle分包进行命名
//index.js if(hash === '#posts'){ import(/* webpackChunkName:'posts' */,'./posts/posts').then(({default: posts}) => { mainElement.appendChild(posts()) //创建界面上的元素 }) }else if(hash === '#album') { import(/* webpackChunkName:'album' */,'./album/album').then(({default:album}) => { mainElement.appendChild(album()) }) } -
生成的bundle就会用注释的名称起名
MiniCssExtractPlugin
-
提取CSS到单个文件
//webpack.config.js const MiniCssExtractPlugin = require('html-webpack-plugin') module: { rules: [ test: /\.css$/, use: [ MiniCssExtractPlugin.loader, //将样式通过style标签注入 ] ] } -
css超过50kb才考虑单独提取
OptimizeCssAssetsWebpackPlugin
-
压缩输出的CSS文件
yarn add optimize-css-assets-webpack-plugin --dev//webpack.config.js const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') plugin: [ new OptimizeCssAssetsWebpackPlugin() ] -
官方建议压缩插件配置到
minimizer中//webpack.config.js optimization: { minimizer: [ new OptimizeCssAssetsWebpackPlugin() ] }yarn webpack --mode production -
此时,默认JS文件不被压缩了,要重新配置内置的
terser-webpack-pluginyarn add terser-webpack-plugin --dev//webpack.config.js const TerserWebpackPlugin = require('terser-webpack-plugin') optimization: { minimizer: [ new TerserWebpackPlugin() ] } -
JS和CSS文件都被压缩了
输出文件名Hash
-
部署文件,会启动静态资源缓存,对于用户的浏览器而言,会缓存住静态资源,后续就不用再请求服务器得到静态资源文件。
-
不过,开启客户端的静态资源缓存,如果在缓存策略中,缓存失效时间过短,效果不会很明显,时间过长,更新无效
-
建议,生成模式下,文件名使用
Hash,一旦资源文件发生改变,文件名称也会发生变化//webpack.config.js output / MiniCssExtractPlugin : //filename: '[name]-[hash].bundle.xx' filename: '[name]-[contenthash:8].bundle.xxx' //根据输出文件内容输出哈希值,不同的文件就有不同的哈希值,最适合解决缓存问题
Rollup
介绍
- ESM打包器
- 更为小巧
- 不支持HMR
- 提供充分利用ESM各项特性的高效打包器
上手
yarn add rollup --dev
参数指定打包入口文件 + 输出格式 + 输出文件路径
yarn rollup ./src/index/js --format iife --file dist
配置文件
//rollup.config.js
//导出一个配置对象
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',//输出文件名
format: 'iife',//输出格式
}
}
需要指定配置文件读取:
yarn rollup --config
或者指定不同配置文件的名称
yarn rollup --config rollup.config.js
使用插件
扩展,且插件时rollup唯一扩展途径:
- 加载其他类型资源模块
- 导入CommonJS模块
- 编译ECMAScript新特性
e.g.rollup-plugin-json:加载json类型模块
yarn add rollup-plugin-json --dev
//rollup.config.js
import json from 'rollup-plugin-json'
plugin:[
json()
//将调用结果放在数组中
]
可以直接使用package.json中的参数
//index.js
import {name,version} from '../package.json'
log(name)
在打包结果bundle.js里就出现了name字段的参数
加载NPM模块
rollup-plugin-node-resolve:直接使用模块名称导入对应模块
yarn add rollup-plugin-node-resolve --dev
//rollup.config.js
import resolve from 'rollup-plugin-node-resolve'
plugin: [
resolve()
]
直接可以导入npm模块:
//index.js
import _ from 'loadsh-es'
log(_.camelCase('111'))
加载commonJS
rollup-plugin-commonjs
//rollup.config.js
import commonjs from 'rollup-plugin-commonjs'
plugins: [
commonjs()
]
新建cjs-module.js
module.exports = {
foo: 'bar'
}
//index.js
import cjs from './cjs-module'//默认导出
log(cjs)
打包后,bundle.js以默认对象导出了结果
代码拆分
Dynamie Imports:动态导入,按需加载
Code Splitting:自动处理代码的拆分
//index.js
import('./logger').then({log}) => {
//结构的方式提取log方法
log('111')
}
不允许IIFE输出格式,要实现代码拆分必须使用amd(浏览器环境)或者commojs标准
yarn rollup --config --format amd
//覆盖掉文件的format设置
当输出多个文件时,不允许使用file模式
//rollup.config.js
export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'amd'
}
}
yarn rollup --config
多入口打包
//rollup.config.js
//方式1
input: ['src/index.js','src/album.js']
//方式2
input: {
foo: 'src/index.js',
bar: 'src/album.js'
}
//内部打包不会自动提取公共模块,所以format必须是amd模式
- 对于amd输出格式,不能直接引用到页面上,必须要通过html标准库引用
新建dist/index.html
<sript src="foo.js"></sript>
Rollup / Webpack 选用规则
Rollup优势:
- 输出结果更加扁平
- 自动移除未引用代码
- 打包结果依然完全可读
缺点:
- 加载非ESM的第三方模块比较复杂
- 模块最终都被打包到一个函数中,无法实现
- 浏览器环境中,代码拆分功能依赖AMD库
webpack:开发应用程序
rollup:JavaScript框架、类库
Parcel
- 零配置的前端应用打包器
- 初始化
package.json
yarn init
- 安装
parcel
yarn add parcel-bundle --dev
- 新建
src/index.html编写开发阶段的源代码,也是打包的入口文件
<script src="main.js"></script>
- 新建
main.js和foo.js
//foo.js
export default {
bar: () => {
console.log('2')
}
}
//main.js
import foo from './foo'
import $ from 'jquery'
import './style.css'
import logo from './zce.png'
$(document.body).append('<h1>hi</h1>')
//打包时可以自动安装jquery模块,也可以加载其它类型模块
//也可以支持资源模块动态导入
import('jquery').then($ => {
$(document.body).append(`<img src="${logo}"/>`)
})
foo.bar()
//使用热加载
if(module.hot){
module.hot.accept(() => {
console.log('1')
})
}
- 运行打包命令
yarn parcel src/index.html
- 以生产模式进行打包
yarn parcel build src/index.html
- webpack相比有更好的生态
规范化标准
- 为什么
软件开发需要多人协同,不同开发者具有不同的编码习惯和喜好增加项目维护成本,需要明确一个统一的标准
- 哪些地方需要
- 代码、文档、提交日志
- 实施规范化的方法
- 编码前人为标准约定
- 通过工具实现Lint
ESLint结合webpack
- 最为主流的JavaScript Lint工具监测JS代码质量
- 统一开发者的编码风格
- 帮助开发者提升编码能力
ESLint工具使用
-
初始化项目
- 创建管理依赖文件package.json
npm init --dev -
安装ESLint模块为开发依赖
npm install eslint --save-dev npx eslint --version npx eslint --init -
通过CLI命令验证结果
ESL检查步骤:
-
编写“问题代码”
-
使用eslint执行检测
- 当语法出现问题时,eslint是不会校验语法规则和风格的
npx eslint .\01.js --fix -
完成eslint使用配置
ESLint配置文件解析
- eslint可以通过package.json的
eslintConfig属性配置
-
parserOptions:ESLint 允许你指定你想要支持的 JavaScript 语言选项。默认情况下,ESLint 支持 ECMAScript 5 语法。你可以覆盖该设置,以启用对 ECMAScript 其它版本和 JSX 的支持。
-
parser:ESLint 默认使用 Espree 作为其解析器,你可以在配置文件中指定一个不同的解析器,只要该解析器符合下列要求:
- 它必须是一个 Node 模块,可以从它出现的配置文件中加载。通常,这意味着应该使用 npm 单独安装解析器包。
- 它必须符合 parser interface。
-
processor:插件可以提供处理器。处理器可以从另一种文件中提取 JavaScript 代码,然后让 ESLint 检测 JavaScript 代码。或者处理器可以在预处理中转换 JavaScript 代码。若要在配置文件中指定处理器,请使用 processor 键,并使用由插件名和处理器名组成的串接字符串加上斜杠。
-
env:一个环境定义了一组预定义的全局变量。
-
globals:当访问当前源文件内未定义的变量时,no-undef 规则将发出警告。如果你想在一个源文件里使用全局变量,推荐你在 ESLint 中定义这些全局变量,这样 ESLint 就不会发出警告了。你可以使用注释或在配置文件中定义全局变量。
-
plugins:ESLint 支持使用第三方插件。在使用插件之前,你必须使用 npm 安装它。
-
rules:ESLint 附带有大量的规则。你可以使用注释或配置文件修改你项目中要使用的规则
为了在文件注释里配置规则,使用以下格式的注释:
/* eslint eqeqeq: "off", curly: "error" */
在这个例子里,eqeqeq 规则被关闭,curly 规则被打开,定义为错误级别。你也可以使用对应的数字定义规则严重程度:
/* eslint eqeqeq: 0, curly: 2 */
这个例子和上个例子是一样的,只不过它是用的数字而不是字符串。eqeqeq 规则是关闭的,curly 规则被设置为错误级别。
如果一个规则有额外的选项,你可以使用数组字面量指定它们,比如:
/* eslint quotes: ["error", "double"], curly: 2 */
这条注释为规则 quotes 指定了 “double”选项。数组的第一项总是规则的严重程度(数字或字符串)。
-
settings:ESLint 支持在配置文件添加共享设置。你可以添加 settings 对象到配置文件,它将提供给每一个将被执行的规则。如果你想添加的自定义规则而且使它们可以访问到相同的信息,这将会很有用,并且很容易配置。
-
extends:一个配置文件可以被基础配置中的已启用的规则继承。
定制ESLint校验规则
ESLint 附带有大量的规则,你可以在配置文件的 rules 属性中配置你想要的规则。每一条规则接受一个参数,参数的值如下:
- "off" 或 0:关闭规则
- "warn" 或 1:开启规则,warn 级别的错误 (不会导致程序退出)
- "error" 或 2:开启规则,error级别的错误(当被触发的时候,程序会退出)
举个例子,我们先写一段使用了平等(equality)的代码,然后对 eqeqeq 规则分别进行不同的配置。
// demo.js
var num = 1
num == '1'
- 这里使用了命令行的配置方式,如果你只想对单个文件进行某个规则的校验就可以使用这种方式:
我们看下 quotes 规则,根据官网介绍,它支持字符串和对象两个配置项:
{
"rules": {
// 使用数组形式,对规则进行配置
// 第一个参数为是否启用规则
// 后面的参数才是规则的配置项
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
]
}
}
扩展
扩展就是直接使用别人已经写好的 lint 规则,方便快捷。扩展一般支持三种类型:
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"eslint-config-standard",
]
}
eslint:开头的是 ESLint 官方的扩展,一共有两个:eslint:recommended、eslint:all。plugin:开头的是扩展是插件类型,也可以直接在plugins属性中进行设置,后面一节会详细讲到。- 最后一种扩展来自 npm 包,官方规定 npm 包的扩展必须以
eslint-config-开头,使用时可以省略这个头,上面案例中eslint-config-standard可以直接简写成standard。
如果你觉得自己的配置十分满意,也可以将自己的 lint 配置发布到 npm 包,只要将包名命名为 eslint-config-xxx 即可,同时,需要在 package.json 的 peerDependencies 字段中声明你依赖的 ESLint 的版本号。
ESLint对TypeScript支持
//.eslintrc.js
parser:'@typescript-eslint/parset'
//语法解析器
ESLint结合自动化工具或者Webpack
结合自动化工具gulp
-
在script函数,在babel之前添加插件,处理源代码:
const script = () => { return src('src/assets/script/*.js', {base: 'src'}) .pipe(plugins.eslint()) .pipe(plugins.eslint.format()) .pipe(plugins.eslint.failAfterError()) .pipe(plugins.babel({presets:[@babel/preset-env]})) ... } -
完成配置文件初始化:
npx eslint -
在eslint配置文件设置:
globals: { '$': 'readonly' } -
成功执行gulp任务:
npx gulp script
结合webpack,通过loader引入
❄在babel之后、style之前配置:
//webpack.config.js
rules:[
...
{
test:/\.js$/,
exclude:/node_modules/,
enforce:'pre'
}
...
]
报错,react要靠额外插件、定义规则
npm install eslint-plugin-react
//eslintrc.js
rules: [
'react/jsx-uses-react': 2 ,
'react/jsx-uses-react': 2
],//解决react定义没有使用的报错
plugins: [
'react'
]
基于ESLint的衍生工具
- Stylelint
- Prettier
- Git Hooks
Stylelint工具的使用
- 提供默认的代码检查规则
- 提供 CLI 工具,快速调用
- 通过插件支持 Sass Less PostCSS
- 支持 Gulp 或 Webpack 集成
本文首发于我的GitHub博客,其它博客同步更新。