一、模块化开发
common.js规范
一个文件就是一个模块
每个模块都有单独的作用域
通过module.exports导出成员
通过require函数载入模块
commonJS是以同步模式加载模块
AMD(异步的模块定义规范)
Require.js
ES Modules
基本特性
自动采用严格模式
每个ESM模块都是单独的私有作用域
ESM是通过CORS去请求外部JS模块的
ESM的script标签会延迟执行脚本
导入和导出
import {} from './modules'
import * as mod from './modules.js'
import ('./modules.js').then(modules=>{
console.log(modules)
})
import title, { name, age } from './modules.js'
直接导出导入成员
1、将多个模块统一在一个文件导出,在从统一入口进行引用
2、使用polifill解决浏览器不兼容ESmodules的问题(只适用于本地测试开发)
< script nomodule src = " ..." > < script>
在node环境下使用ES Modules编写代码
1、将文件扩展名从js改为mjs
import { foo, bar} from './modules.mjs'
2、使用node --experimental-module执行mjs文件
node --experimental-modules index.mjs
注意事项
1、系统内置成员可以通过ES module的提取成员方式导入,也可以默认导入
import fs from 'fs'
import { writeFileSync } from 'fs'
2、第三方模块不支持直接提取成员,因为第三方模块都是默认导出
import { _cameCase } from 'lodash'
import _ from 'lodash'
console.log(_comCase('ES Module'))
ES modulees 与Common JS模块交互
可以在ES Module中导入commonJS模块
modules.exports = {
foo: 1111
}
====>
exports.foo = 111;
import mod from './commonjs.js'
console.log(mod) // { foo :111}
import { foo } from './commonjs.js'
CommonJs中不能导入ES Module模块
CommonJs始终只会导出一个默认成员
注意import不是解构导出对象
在node的最新版本中,在package.json中添加type字段,就表示该工程默认使用ES Module编写代码,这意味着可以不用将js文件改为mjs,不过此时如果还需要使用commonJs,需要将CommonJS模块文件改为cjs后缀名
{
type: "module"
}
node --exprimental-modules index.js
node --exprimental-modules common.cjs
二、模块化打包工具
模块化打包工具的由来
新特性代码编译
模块化JavaScript打包
执行不同类型的资源文件
模块化打包工具概要
打包工具解决的是前端整体的模块化,并不是单指JavaScript模块化
webpack
资源文件加载
样式文件加载
const path = require ( 'path' ) ;
module . exports = {
mode: 'none' ,
entry: './src/index.js' ,
output: {
filename: 'bundle.js' ,
path: path. join ( __dirname, 'dist' )
} ,
module : {
rules: [
{
test: /.css$/ ,
use: [
'style-loader' ,
'css-loader'
]
}
]
}
}
文件资源加载
const path = require ( 'path' ) ;
module . exports = {
mode: 'none' ,
entry: './src/index.js' ,
output: {
filename: 'bundle.js' ,
path: path. join ( __dirname, 'dist' )
} ,
module : {
rules: [
{
test: /.css$/ ,
use: [
'style-loader' ,
'css-loader'
]
}
]
}
}
常用加载器分类
编译转换类,转换为JS代码,如css-loader
文件操作类,将资源文件拷贝到输出目录,将文件访问路径向外导出,如:file-loader
代码检查器,统一代码风格,提高代码质量,如:eslint-loader
webpack 处理ES2015
因为模块打包需要,所以webpack可以处理import和export,除此之外,并不能转换其他的ES6特性。如果想要处理ES6,需要安装转化ES6的编译型loader,最常用的就是babel-loader,babel-loader依赖于babel的核心模块,@babel/core和@babel/preset-env
{
test: /.js$/ ,
use: {
loader: 'babel-loader' ,
options: {
presets: [ '@babel/preset-env' ]
}
} ,
exclude: /(node_modules)/ ,
}
注意:Webpack只是打包工具,加载器可以用来编译转化代码
加载资源的方式
遵循ES Modules标准的import声明
遵循CommonJS标准的require函数。对于ES的默认导出,要通过require('./XXX').default的形式获取
遵循AMD标准的define函数和require函数
Loader加载的非JavaScript也会触发资源加载
css-loader在处理css代码时,遇到url函数,会将这个资源文件 交给url-loader处理
webpack的核心工作原理
核心工作原理:
根据配置找到打包入口文件
顺着入口文件代码里的 import 和 require之类的语句
解析推断文件所依赖的资源模块
分别去解析每个资源模块对应的依赖,最后形成一颗依赖树
递归依赖树,找到每个节点对应的资源文件
根据配置文件 rules 属性,找到资源模块所对应的加载器,交给对应的加载器加载对应的资源模块
最后将加载以后的结果放入到bundle.js打包结果里
实现整个项目的打包。
webpack Loader的工作原理
loader机制是webpack的核心特性之一。每个 Webpack 的 Loader 都需要导出一个函数,这个函数就是我们这个 Loader 对资源的处理过程,它的输入就是加载到的资源文件内容,输出就是我们加工后的结果。我们通过 source 参数接收输入,通过返回值输出。
对于返回的输出,有两种思路:
Webpack 加载资源文件的过程类似于一个工作管道,你可以在这个过程中依次使用多个 Loader,但是最终这个管道结束过后的结果必须是一段标准的 JS 代码字符串。
const marked = require ( 'marked' )
module. exports = source => {
const html = marked ( source)
const code = ` export default ${ JSON . stringify ( html) } `
return code
}
插件机制
插件机制的是webpack的另一个核心特性,目的是为了增强webpack自动化方面的能力。
常见的插件介绍
CleanWebpackPlugin、HtmlWebpackPlugin、CopyWebpackPlugin
插件使用总结
webpack开发体验问题
自动进行编译:npx webpack --watch
会监视文件的变化自动进行打包
自动打开浏览器: npx webpack-dev-server --open
source map
Source Map解决了源代码与运行代码不一致所产生的问题.Webpack 支持sourceMap 12种不同的方式,每种方式的效率和效果各不相同。效果最好的速度最慢,速度最快的效果最差.下面是几种常用方式介绍:
eval- 是否使用eval执行代码模块
cheap- Source map是否包含行信息
module-是否能够得到Loader处理之前的源代码
inline- SourceMap 不是物理文件,而是以URL形式嵌入到代码中
hidden- 看不到SourceMap文件,但确实是生成了该文件
nosources- 没有源代码,但是有行列信息。为了在生产模式下保护源代码不被暴露
开发模式推荐使用:eval-cheap-module-source-map,原因:
代码每行不会太长,没有列也没问题
代码经过Loader转换后的差异较大
首次打包速度慢无所谓,重新打包相对较快
生产模式推荐使用:none,原因:
Source Map会暴露源代码
调试是开发阶段的事情
对代码实在没有信心可以使用nosources-source-map
devtool
webpack HRM
HMR(Hot Module Replacement) 模块热替换,应用运行过程中,实时替换某个模块,应用运行状态不受影响。
webpack-dev-server自动刷新导致的页面状态丢失。我们希望在页面不刷新的前提下,模块也可以即使更新。热替换只将修改的模块实时替换至应用中。
HMR是webpack中最强大的功能之一,极大程度的提高了开发者的工作效率。
HMR已经集成在了webpack-dev-server中,运行webpack-dev-server --hot,也可以通过配置文件开启.
Webpack中的HMR并不是对所有文件开箱即用,样式文件支持热更新,脚本文件需要手动处理模块热替换逻辑。而通过脚手架创建的项目内部都集成了HMR方案。
HMR注意事项:
处理HMR的代码报错会导致自动刷新
没启动HMR的情况下,HMR API报错
代码中多了很多与业务无关的代码
生产环境优化
我们在生产环境中,更注重开发效率,而在生产环境中,更注重开发效率。
模式(mode)
webpack建议我们为不同的环境创建不同的配置,两种方案:
const path = require ( 'path' )
const webpack = require ( 'webpack' )
const { CleanWebpackPlugin} = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
const CopyWebpackPlugin = require ( 'copy-webpack-plugin' )
module . exports = ( env, argv ) => {
const config = {
mode: 'none' ,
entry: './src/main.js' ,
output: {
filename: 'bundle.js' ,
path: path. join ( __dirname, 'dist' ) ,
} ,
module : {
rules: [
{
test: /.md$/ ,
use: [ 'html-loader' , './markdown-loader.js' ]
}
]
} ,
plugins: [
new CleanWebpackPlugin ( ) ,
new HtmlWebpackPlugin ( {
title: 'Webpack Plugin Sample' ,
meta: {
viewport: 'width=device-width'
} ,
template: './src/index.html'
} ) ,
new HtmlWebpackPlugin ( {
filename: 'about.html'
} ) ,
new webpack. HotModuleReplacementPlugin ( )
] ,
devServer: {
contentBase: './public' ,
proxy: {
'/api' : {
target: 'https://api.github.com' ,
pathRewrite: {
'^/api' : ''
} ,
changeOrigin: true ,
}
} ,
hotOnly: true ,
} ,
devtool: 'eval-cheap-module-source-map'
}
if ( env === 'production' ) {
config. mode = 'production'
config. devtool = false
config. plugins = [
... config. plugins,
new CleanWebpackPlugin ( ) ,
new CopyWebpackPlugin ( {
patterns: [ 'public' ]
} )
]
}
return config
}
Webpack.common.js
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module . exports = {
entry: './src/main.js' ,
output: {
filename: ` bundle.js `
} ,
module : {
rules: [
{
test: /.js$/ ,
use: {
loader: 'babel-loader' ,
options: {
presets: [ '@babel/preset-env' ]
}
}
}
]
} ,
plugins: [
new HtmlWebpackPlugin ( {
filename: ` index.html `
} )
]
}
Webpack.dev.js
const common = require ( './webpack.common' )
const merge = require ( 'webpack-merge' )
module . export = merge ( common, {
mode: 'development' ,
} )
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 ( {
patterns: [ 'public' ]
} )
]
} )
Package.json
"scripts": {
"server": "npx webpack serve --config webpack.dev.js --open",
"build": "webpack --config webpack.prod.js"
},
webpack的优化配置
definePlugin
DefinePlugin 为代码注入全局成员,这个内置插件默认就会启动,往每个代码中注入一个全局变量process.env.NODE_ENV
const webpack = require ( 'webpack' )
plugins: [
new HtmlWebpackPlugin ( {
filename: ` index.html `
} ) ,
new webpack. DefinePlugin ( {
API_BASE_URL : JSON . stringify ( 'http://api.example.com' )
} )
] `
Tree-Shaking 摇掉代码中未引用到的代码(dead-code),这个功能在生产模式下自动被开启。Tree-Shaking并不是webpack中的某一个配置选项,而是一组功能搭配使用后的效果。因为Tree-Shaking前提是ES Modules,由Webpack打包的代码必须使用ESM,为了转化ES中的新特性,会使用babel处理新特性,就有可能将ESM转化CommonJS,而我们使用的@babel/preset-env这个插件集合就会转化ESM为CommonJS,所以Tree-Shaking会不生效。但是在最新版babel-loader关闭了转换ESM的插件,所以使用babel-loader不会导致Tree-Shaking失效
optimization: {
usedExports : true,
minimize : true
}
合并模块函数 concatenateModules, 又被成为Scope Hoisting,作用域提升
optimization: {
usedExports : true,
minimize : true,
concatenateModules : true
}
sideEffects 副作用,指的是模块执行时除了导出成员之外所做的事情,sideEffects一般用于npm包标记是否有副作用。如果没有副作用,则没有用到的模块则不会被打包。
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true,
sideEffects: true
}
在package.json里面增加一个属性sideEffects,值为false,表示没有副作用,没有用到的代码则不进行打包。确保你的代码真的没有副作用,否则在webpack打包时就会误删掉有副作用的代码,比如说在原型上添加方法,则是副作用代码;还有CSS代码也属于副作用代码。
代码分割
webpack的一个弊端:所有的代码都会被打包到一起,如果应用复杂,bundle会非常大。而并不是每个模块在启动时都是必要的,所以需要分包、按需加载。物极必反,资源太大了不行,太碎了也不行。太大了会影响加载速度;太碎了会导致请求次数过多,因为在目前主流的HTTP1.1有很多缺陷,如同域并行请求限制、每次请求都会有一定的延迟,请求的Header浪费带宽流量。所以模块打包时有必要的。
目前的webpack分包方式有两种:
多入口打包:适用于多页应用程序,一个页面对应一个打包入口,公共部分单独抽取。
entry: {
index: './src/index.js' ,
album: './src/album.js'
} ,
output: {
filename: '[name].bundle.js'
} ,
plugins: [
new HtmlWebpackPlugin ( {
title: 'Multi Entry' ,
template: './src/index.html' ,
filename: 'index.html' ,
chunks: [ 'index' ]
} ) ,
new HtmlWebpackPlugin ( {
title: 'Nulti Entry' ,
template: './src/album.html' ,
filename: 'album.html' ,
chunks: [ 'album' ]
} )
] ,
optimization: {
splitChunks: {
chunks: 'all'
}
}
动态导入:需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包。通过动态导入生成的文件只是一个序号,可以使用魔法注释指定分包产生bundle的名称。相同的chunk名会被打包到一起。
import ( './post/posts' ) . then ( { default : posts} ) = > {
mainElement. appendChild ( posts ( ) )
}
MiniCssExtractPlugin可以提取CSS到单个文件
当css代码超过150kb左右才建议使用。
const MiniCssExtracPlugin = require ( 'mini-css-extract-plugin' )
module : {
rules: [
{
test: /.css$/ ,
use: [
/ / 'style-loader' ,
MiniCssExtracPlugin . loader,
'css-loader'
]
}
]
} ,
OptimizeCssAssetsWebpackPlugin 压缩输出的CSS文件
webpack仅支持对js的压缩,其他文件的压缩需要使用插件。
可以使用 optimize-css-assets-webpack-plugin压缩CSS代码。放到minimizer中,在生产模式下就会自动压缩
optimization: {
minimizer : [
new TerseWebpackPlugin ( ) , // 指定了minimizer说明要自定义压缩器,所以要把JS的压缩器指指明,否则无法压缩
new OptimizeCssAssetWebpackPlugin ( )
]
}
输出文件名hash
生产模式下,文件名使用Hash
项目级别的hash
output: {
filename : '[name]-[hash].bundle.js'
} ,
chunk级别的hash
output: {
filename : '[name]-[chunkhash].bundle.js'
} ,
文件级别的hash,:8是指定hash长度 (推荐)
output: {
filename : '[name]-[contenthash:8].bundle.js'
} ,