目前前端模块化最佳实践方式
目前在在node环境下使用CommonJS规范,
浏览器环境下使用ES Modules规范
ES Modules基本特性
通过给script标签设置type = "module" 属性,就可以以ES Modules的标准执行其中的代码
- ES Modules自动采用严格模式,忽略'use strict'
- 每个ES Modules都运行在独立的私有作用域中
- ES Modules通过CORS的方式请求外部JS模块
- ES Modules的script标签会被延迟执行,等同于defer属性
ES Modules导入和导出
// 导出 ./module.js
const foo = '111'
export { foo }
// 导入 ./app.js
import { foo } from './module.js'
导出重命名
// 导出
const foo = '111'
export { foo as bar }
// 导入
import { bar } from './module.js'
重命名中,如果将导出成员重命名为default,那么该成员会作为模块的默认成员导出
const foo = '123'
export { foo as default}
在导入时,必须重新命名,因为default是关键词
import { default as bar } from './module.js'
关于导出默认值也可以通过export default设置,导入时变量名可以随便定义
const name = '123'
export default name
注意事项
- 暴露出来的内容在引入时不允许进行修改,是只读的
import { name } from './module.js'
name = 123 // error
- exrpot {} 是固定语法,后面的{}并不是对象的解构,如果导出解构的对象可以通过export default {},
同时import {} 也是固定用法,不是对象解构 - ES Module导出时导出的是成员的引用,在b中引用a中导出的成员,相当于把在a中定义的导出成员的内存空间引用关系给到b,也就是引用的值会受模块内的值的修改的影响
动态加载模块
当需要动态加载模块时,需要使用import(文件路径),该函数返回的是Promise
let href = './module
import(href).then(module => {
// 模块对象可以通过参数拿到
console.log(module)
})
直接导出导入模块
直接将导入成员作为当前模块的导出成员,在当前作用域不能访问这些成员
export { a, b} from './module'
在node中使用ES Modules
node 老版本8.5+
- 在node中使用ES Module需要将文件扩展名由js改为mjs
- 启动时需要加上--experimental-modules
node --experimental-modules index.mjs
- ES Modules中可以导入CommonJS模块
- CommonJS中不能导入ES Modules模块
- CommonJS始终只会导出一个默认成员
新版本,通过设置package.json中的type:module字段就可以以ES Modules规范工作,
如果后续要使用CommonJS,则需要修改拓展名为.cjs
模块打包工具
打包工具解决的是前端整体的模块化,而不是单独的JS模块化
webpack
webpack作为一个模块打包器,可以讲零散的模块化JS代码打包同一个文件当中,
在打包过程中,可以通过模块加载器(Loader)将有环境兼容问题的代码转换,
同时还具有拆分的能力,可以根据自身需要将所有代码打包从而避免所有代码打包进一个文件从而导致体积过大的问题,
比如我们可以讲初次运行必须的代码打包到一起,其余代码单独存放,等应用运行需要的时候在异步加载这个模块,
从而实现渐进式加载,从而避免文件太碎或者文件太大的问题。
webpack也支持在JS当中以模块化的方式载入任意资源文件
打包
webpack 4.0+ 默认会把src/index.js作为打包入口,把打包后的内容存在dist/main.js
当然也可以根据自己需要进行配置
- 创建webpack.config.js,这个文件是一个运行在Node环境的JS文件,所以需要按照CommonJS的规范编写代码,
该文件可以导出一个对象,通过导出对象的属性就可以完成配置选项
const path = require('path')
module.exports = {
// 设置工作模式
mode: 'development', // 开发模式development,生产模式production,无任何优化处理预设none,
// 指定打包入口路径
entry: './src/main.js',
// 输出文件
output: {
// 输出文件名称
filename: 'bundle.js',
// 文件目录 必须是绝对路径,通过node模块转换
path: path.join(__dirname, 'output')
}
}
资源模块加载
webpack默认只会处理JS文件,如果需要打包其他资源则需要配置加载器
loader是前端整体模块化的核心,借助不同的loader就可以加载任何类型的资源
// 安装css加载器
cnpm install css-loader --save-dev
// 安装style加载器 将css-loader的转化结果通过style的形式追加到页面上
cnpm install style-loader --save-dev
const path = require('path')
module.exports = {
// 指定打包入口路径
entry: './src/index.js',
// 输出文件
output: {
// 输出文件名称
filename: 'bundle.js',
// 文件目录 必须是绝对路径,通过node模块转换
path: path.join(__dirname, 'output')
},
module: {
// 资源加载模块规则配置
rules: [
// 每个规则对象都需要设置两个属性
{
test: /.css$/, // 正则,匹配文件路径
use: [
'style-loader',
'css-loader'
] // 匹配到的文件需要使用的loader, 如果设置多个loader,loader的执行顺序为从后往前执行
}
]
}
}
文件资源加载器
cnpm install file-loader --save-dev
const path = require('path')
module.exports = {
// 指定打包入口路径
entry: './src/index.js',
// 输出文件
output: {
// 输出文件名称
filename: 'bundle.js',
// 文件目录 必须是绝对路径,通过node模块转换
path: path.join(__dirname, 'output'),
publicPath: 'dist/' // 默认值为空字符串,指项目中引用css,js,img等资源时候的一个基础路径,
},
module: {
// 资源加载模块规则配置
rules: [
// 每个规则对象都需要设置两个属性
{
test: /.css$/, // 正则,匹配文件路径
use: [
'style-loader',
'css-loader'
] // 匹配到的文件需要使用的loader, 如果设置多个loader,loader的执行顺序为从后往前执行
},
{
test: /.png$/,
use: 'file-loader'
}
]
}
}
URL加载器
Data URLs是一种特殊的文件协议,可以字节用来表示一个文件,传统url一般要求服务器上有一个对应的文件,
通过请求地址得到这个文件。
协议 媒体类型和编码 文件内容
data:[][;base64],
使用这种方式url就不会发送任何http请求
data:text/html;charset=UTF-8,html content
如果是图片或字体这种类型就会进行base64编码[;base64]
data:image/png;base64,sdjhwkwjhakuw....(base64编码)
使用url-loader进行转换
module: {
// 资源加载模块规则配置
rules: [
// 每个规则对象都需要设置两个属性
{
test: /.png$/,
use: {
loader: 'url-loader',
options: { // 这种房使用url-loader需要配合file-loader
// 超出限制大小的文件会使用file-loader
limit: 10 * 1024 // 字节*1024 = kb
}
}
}
]
}
一般项目中小文件使用url-loader转成DataURLs,减少http请求次数,较大文件单独存放,提高加载速度
常用加载器分类
编译转换类
把加载到的资源模块转换为JS代码
css代码通过css-loader转换为bundle.js中以JS形式工作的css模块
index.css --- css-loader ---> bundle.js
文件操作类
把加载到的资源模块拷贝到输出目录并将访问路径向外导出
代码检查类
统一代码风格,提高代码质量
webpack模块加载方式
webpack兼容多种模块化标准,除非必要情况,尽量不要混合使用
- 遵循ES Modules标准的import声明
- 遵循CommonJS标准的require函数(如果通过require导入一个ESModule的话,对于默认导出,使用.default属性获取)
let a = require('../demo/myPromise').default
- 遵循AMD标准的define函数和require函数
- 样式代码中的@import指令和url函数
- HTML代码中图片标签的src属性
// 通过配置html-loader配置
module: {
// 资源加载模块规则配置
rules: [
// 每个规则对象都需要设置两个属性
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
}
webpack插件机制
loader专注实现资源模块加载,plugin解决其他自动化工作
plugin通过钩子机制实现,在webpack工作时会有很多环节,
为了便于插件扩展,webpack几乎在每个环节上都埋下了钩子,
开发插件时通过往不同的节点上挂载不同的任务扩展webpack能力
自动清除输出目录插件
cnpm install clean-webpack-plugin
module: {
plugins: [
new CleanWebpackPlugin()
]
}
自动生成HTML插件
借助html-webpack-plugin插件自动生成html
// 由于html自动生成在dist目录,所以可以删除publicPath属性
plugins: [
// 用于生成index.html
new htmlWebpackPlugin({
title: 'asd', // html中title
meta: { // meta 标签
viewprot: 'width=device-width
},
template: './src/index.html', //对于大量需要配置的html页面可以通过模板来配置
}),
// 用于生成about.html
new htmlWebpackPlugin({
filename: 'about.html
})
]
静态文件
通过copy-webpack-plugin插件拷贝静态资源
plugins: [
// 开发中尽量不要使用这个插件
// 因为开发中会频繁的执行打包任务
// 当静态资源比较多或比较大时,打包中的开销就会比较大,降低开发效率
// 一般都在上线前使用该插件
new copyWebpackPlugin([
'public'
])
]
开发插件
webpack要求插件必须是一个函数或者包含apply方法的对象,
一般会把插件定义为了一个类型,在类型中定义一个apply方法,
使用时通过类型定义一个实例来使用
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
class MyPlugin {
// apply方法在webpack启动时自动被调用
apply(compiler) { // compiler参数包含了webpack所有配置信息
console.log('启动')
// 通过tap方法注册钩子
// tap方法接受两个参数,第一个是插件名称,第二个是挂载到钩子上的函数
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation 此次打包过程的上下文
// 通过assets属性获取即将写入目录的资源文件信息
for (let name in compilation.assets) {
console.log(name) // 文件名称
console.log(compilation.assets[name].source()) // 内容
let str = 'asdawdasdawwdawd'
if (name.endsWith('.js')){ // 判断是否以.js结尾
compilation.assets[name] = {
source: () => str, // 替换内容
size: () => str.length // 内容大小,webpack要求的必须的方法
}
}
}
})
}
}
module.exports = {
entry: './src/main.js',
output: {
filename: "bundle.js",
path: path.join(__dirname, 'dist'),
publicPath: "dist/"
},
plugins: [
new CleanWebpackPlugin(),
new MyPlugin()
]
}
watch工作模式
在watch工作模式下,监听文件变化,自动重新打包
npx webpack --watch
Dev Server
集成自动编译和自动刷新浏览器等功能
cnpm install webpack-dev-server --dev
// 使用这个命令,内部会自动打包应用并启动一个httpServer运行打包结果,同时监听文件变化
// --open会自动打开浏览器
npx webpack-dev-server --open
webpack-dev-server为了提高运行效率,并没有将打包后的结果写入磁盘当中,而是暂时存放在内存中,从而减少磁盘读写操作,大大提高开发效率
通过配置webpack提供静态资源访问
devServer: {
// 额外为开发服务器指定查找资源目录
contentBase: './public',
// api代理
proxy: {
'/api': {
// https://localhost:8080/api/user ->https://api.github.com/api/user
target: 'https://api.github.com',
// https://localhost:8080/api/user ->https://api.github.com/user
pathRewrite: {
// 把以api开头的替换为空
'^/api': ''
},
// 不使用localhost:8080作为请求的主机名
changeOrigin: true
}
}
}
Source Map
devtool: 'source-map'
热替换/热更新
webpack-dev-server自动刷新会导致页面状态丢失,所以采用HMR模块热替换
HMR可以再应用运行过程中实时替换某个模块,应用状态不受影响
HMR集成在webpack-dev-server中,通过运行webpack-dev-server --hot实现热更新,也可以在配置文件中添加对应的配置
const webpack = require('webpack)
module.exports = {
devServer: {
hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
不同环境下的配置
- 配置文件根据环境不同导出不同配置(适用于中小型项目)
module.exports = (env, argv) => {
// env 通过cli传入的环境名参数,argv 运行cli传入的全部参数
const config = {}
if (env === 'production') {}
return config
}
// 以生产模式运行打包 npx webpack --env production
- 一个环境对应一个配置文件
// 安装用于 与基础配置合并配置
// cnpm install webpack-merge --dev
// webpack.prod.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
module.exports = merge(common, {
mode: 'production',
})
// npx webpack --config webpack.prod.js
DefinePlugin
webpack内置插件,为代码注入全局成员,在production模式下会自动启动,并在全局环境下注入一个process.env.NODE_ENV,
很多第三方模块都是通过判断这个常量来确定当前环境
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({ // 每一对键值都会被注入到代码当中
API_BASE_URL: '"https: //api.example.com"' //传入字符串
})
]
}
Tree Shaking
Tree Shaking前提是必须使用ESModules组织代码
Tree Shaking并不是某一个配置选项,而是一组功能搭配后的效果,生产模式下自动开启,会自动检测代码中未引用的部分并移除掉未引用的代码(dead-code)
// 其他模式下启动tree Shaking
optimiztion: { // 集中配置webpack内置的优化功能
usedExports: true, // 只导出外部使用的成员
minisize: true // 代码压缩
}
webpack合并模块concatenateModules
尽可能将全部模块合并输出到一个函数中,既提升了运行效率,有降低了代码体积
optimiztion: {
usedExports: true,
minisize: true,
concatenateModules: true
}
sideEffects
用来标识代码是否有副作用(除了导出成员是否还做了其他事情),一般用于npm包标记是否有副作用
production模式下会自动开启
optimiztion: {
sideEffects: true
}
// package.json
"sideEffects": false // 标识代码没有副作用(确保你的代码真的没有副作用)
// 或者
"sideEffects": [ // 指定哪些文件有副作用
'./src/main/js',
'*.css'
]
webpack代码分割
所有代码打包到一起会导致最终打包后的bundle.js体积非常大,然而并不是每个模块
在项目启动时都必须要运行的,所以需要进行分包,按需加载,提高响应速度运行效率
webpack的初衷就是把散碎的代码模块打包到一起,因为目前主流的http1.1版本同一个域名下存在并行请求限制,
而且每次请求存在延时,每次请求存在大量的请求头和响应头,浪费带宽流量,所以模块打包是有必要的
但是当应用越来越大的时候,为了避免体积过大造成响应过慢,所以也需要变通,进行代码分割分包处理
webpack目前有两种打包方式
- 多入口打包
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports ={
mode: 'none',
ertry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // name占位符会被替换成对应入口名称
},
plugin: [
new HtmlWebpackPlugin({
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
filename: 'album.html',
chunks: ['album']
})
],
optimization: {
splitChunks: {
chunks: 'all' // 所有公共模块都提取到独立的js当中
}
}
}
- 动态导入
需要用到某个模块时,再加载对应模块,可以极大程度节省带宽和流量
动态导入的模块会被自动分包
只需按照ESModule动态导入成员的方式导入模块就可以了,webpack会自动处理分包
默认通过动态导入产生的bundle.js文件名是一个序号,如果希望改变名称,可以通过魔法注释实现
import(/* webpackChunkName: 'post' */ './src/index').then()
MiniCssExtractPlugin
提取css到单个文件(如果css体积不是很大,就没必要提取,比如文件体积超过150kb,否则可能会适得其反)
cnpm install mini-css-extract-plugin
const MiniExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
user: [
MiniExtractPlugin.loader,
'css-loader'
]
}
]
},
plugin: [
new MiniExtractPlugin()
]
}
OptimizeCssAssetsWebpackPlugin压缩输出的css文件
cnpm install optimize-css-assets-webpack-plugin
// js压缩
cnpm install terser-webpack-plugin
const MiniExtractPlugin = require('optimize-css-assets-webpack-plugin')
const terserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
// 将压缩类插件统一配置在minimizer中,当minimizer开启时统一压缩,
// 设置此配置会替换掉webpack默认压缩位置,所以还需要额外压缩js
new OptimizeCssAssetsWebpackPlugi(),
new terserWebpackPlugin()
]
}
}
webpack输出文件名Hash
部署前端资源文件时会启动服务器的静态资源缓存,如果设置的时间比较短就没有什么效果,
如果设置的比较长就会造成重新部署后没办法及时更新到客户端,为了解决这个问题,推荐在生产模式下,
文件名使用hash值
webpack文件名支持三种hash
- 任何一处代码发生变化那么项目中的所有hash都会发生变化
output: {
filename: '[name]-[hash].bundle.js' // name占位符会被替换成对应入口名称
}
- 修改同一chuank下的hash
output: {
filename: '[name]-[chunkhash].bundle.js' // name占位符会被替换成对应入口名称
}
- 不同文件有不同hash值
output: { // :8 指定hash长度为8位
filename: '[name]-[contenthash:8].bundle.js' // name占位符会被替换成对应入口名称
}
Rollup
与webpack类似,Rollup更为轻巧,仅仅是一款ESModule打包器,充分利用ESModuleg各种特性的高效打包器
cnpm install rollup
npx rollup ./src/index(入口文件) --format iife(输出格式) --file dist/bundle.js(输出路径)
配置文件
// rollup.config.js
export default {
input: 'src/index.js', // 入口文件
output: {
file: 'dist/bundle.js', // 输出文件
format: 'iife' //输出格式
}
}
// npx rollup --config rollup.config.js
使用插件
插件是Rollup唯一的扩展方式
import json from 'rollup-plugin-json'
export default {
input: 'src/index.js', // 入口文件
output: {
file: 'dist/bundle.js', // 输出文件
format: 'iife' //输出格式
},
plugins: [
json()
]
}
加载npm模块
rollup默认只能通过文件路径加载模块,可以使用rollup-plugin-node-resolve插件通过模块名称导入模块
加载commonJS模块
默认只处理ESModule模块, 通过rollup-plugin-commonjs处理commonJS打包
Rollup和webpack选用
rollup打包更加扁平,自动移除未引用代码,打包结果依然可读,加载三方模块复杂,无法实现HRM,代码查分依赖AMD库
webpack更适合应用程序,而框架类库的开发更适合选用rollup
Parcel
零配置的前端应用打包器
parcel建议用html文件作为打包入口
npm init
cnpm install parcel-bundler
npx parcel ./src/index.html
相同体量的项目,parcel打包速度比webpack快,因为parcel内部是多进程打包
规范化标准
ESlint
目前主流的检测JS代码质量的工具,很容易统一开发风格
ESlint安装、校验和使用
// 安装
cnpm install eslint --save-dev
// 校验
npx eslint --version
初始化eslint
- 初始化项目,安装 ESLint 模块为开发依赖 npm install eslint -D
- 编写“问题”代码,使用 eslint 执行检测 npx eslint ./01-prepare.js
- 加上参数 --fix
- 可以自动修复格式问题
- 当代码中存在语法错误时,eslint 没法检查问题代码
- 完成 eslint 使用配置
ESLint 结合 Webpack
- Webpack 可以通过 loader 机制实现 eslint 的检测工作
- 安装 eslint eslint-loader
- 在 webpack.config.js 文件配置 eslint-loader 应用在 .js 文件中
- 安装相关插件,如:eslint-plugin-react
- 修改 .eslintrc.js 的配置
Stylelint
- 提供默认的代码检查规则
- 提供 CLI 工具,快速调用
- 通过插件支持 Sass Less PostCSS
- 支持 Gulp 或 Webpack 集成
eslint结合Git hooks
- 代码提交至仓库之前未执行 lint 工作
- 使用 lint 的目的就是保证提交到仓库的代码是没有问题的
- 通过 Git Hooks 在代码提交前强制 lint
- Git Hooks 也称为 git 钩子,每个钩子都对应一个任务
- 通过 shell 脚本可以编写钩子任务触发时要具体执行的操作