学了一段时间的webpack,总觉得插件太多,麻烦。在学习过程中得知了vue-cli脚手架工具。为啥我学vue的时候没有听说过这个。。
好了,来说一下vue-cli吧。它是一个专门为单页面应用快速搭建繁杂的脚手架,它可以轻松的创建新的应用程序而且可用于自动生成vue和webpack的项目模板。看了webpack四五天之后
就去试了一下这个脚手架,很好用的。之前webpack就想偷偷懒了。直接找了一下别人学习webpack的笔记。嘿嘿。虽然脚手架很方便,但是现在企业都需要知其然,知其所以然。熟悉
webpack源代码还是非常有帮助的。react额话,也有create-react-app这样的脚手架。以后学react的时候在试一试吧。不厚道的我直接拿别人的笔记过来了。
转载
https://github.com/TigerHee/shareJS/blob/master/webpack4.md
安装前先npm初始化
本地服务
复制html
处理css
处理less
抽离css文件,通过link引入
压缩css和js
给css加上兼容浏览器的前缀
es6 转 es5
es 7的语法
全局变量引入
webpack图片打包
当图片小于多少,用base64
打包文件分类
希望输出的时候,给这些cssimg加上前缀,传到服务器也能访问
如果只希望处理图片
打包多页应用
配置source-map
watch 改完代表重新打包实体
webpack的其他三个小插件
webpack 跨域
如果后端给的请求没有API 「跨域」
前端只想单纯mock数据 「跨域」
有服务端,不用代理, 服务端启动webpack 「跨域」
webpack解析resolve
但是每次引入都很长,如何优雅引入
省略扩展名
定义环境变量
区分两个不同的环境
webpack 优化
优化:当某些包是独立的个体没有依赖
优化:规则匹配设置范围
优化:忽略依赖中不必要的语言包
动态链接库
多线程打包happypack
webpack 自带的优化
抽取公共代码
懒加载(延迟加载)
热更新(当页面改变只更新改变的部分,不重新打包)
tapable介绍 - SyncHook
tapable介绍 - SyncBailHook
tapable介绍 - SyncWaterfallHook
tapable介绍 - SyncLoopHook
AsyncParallelHook 与 AsyncParallelBailHook
AsyncParallelHook
AsyncParallelBailHook
异步串行 —— AsyncSeriesHook
异步串行 —— AsyncSeriesWaterfallHook
手写webpack
webpack分析及处理
创建依赖关系
ast递归解析
生成打包工具
增加loader
增加plugins
loader
配置多个loader
babel-loader实现
banner-loader实现(自创)
实现file-loader和url-loader
less-loader和css-loader
css-loader
webpack 中的插件
文件列表插件
内联的webpack插件
打包后自动发布
安装前先npm初始化
npm init -y
npm i webpack webpack-cli -D
let path = require('path') // 相对路径变绝对路径
module.exports = {
mode: 'production', // 模式 默认 production development
entry: './src/index', // 入口
output: {
filename: 'bundle.[hash:8].js', // hash: 8只显示8位
path: path.resolve(__dirname, 'dist'),
publicPath: '' // // 给所有打包文件引入时加前缀,包括css,js,img,如果只想处理图片可以单独在url-loader配置中加publicPath
}
}
本地服务
npm i webpack-dev-server -D
devServer: {
port: 3000,
progress: true // 滚动条
contentBase: './build' // 起服务的地址
open: true // 自动打开浏览器
compress: true // gzip压缩
}
复制html
npm i html-webpack-plugin -D
let HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [ // 放着所有webpack插件
new HtmlWebpackPlugin({ // 用于使用模板打包时生成index.html文件,并且在run dev时会将模板文件也打包到内存中
template: './index.html', // 模板文件
filename: 'index.html', // 打包后生成文件
hash: true, // 添加hash值解决缓存问题
minify: { // 对打包的html模板进行压缩
removeAttributeQuotes: true, // 删除属性双引号
collapseWhitespace: true // 折叠空行变成一行
}
})
]
html-webpack-plugin#options
处理css
npm i css-loader style-loader -D
// css-loader 作用:用来解析@import这种语法
// style-loader 作用:把 css 插入到head标签中
// loader的执行顺序: 默认是从右向左(从下向上)
module: { // 模块
rules: [ // 规则
// style-loader 把css插入head标签中
// loader 功能单一
// 多个loader 需要 []
// 顺便默认从右到左
// 也可以写成对象方式
{
test: /.css$/, // css 处理
// use: 'css-loader'
// use: ['style-loader', 'css-loader'],
use: [
// {
// loader: 'style-loader',
// options: {
// insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖
// }
// },
MiniCssExtractPlugin.loader,
'css-loader', // css-loader 用来解析@import这种语法,
'postcss-loader'
]
}
]
}
处理less
npm i less-loader
{
test: /.less$/, // less 处理
// use: 'css-loader'
// use: ['style-loader', 'css-loader'],
use: [
// {
// loader: 'style-loader',
// options: {
// insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖
// }
// },
MiniCssExtractPlugin.loader, // 这样相当于抽离成一个css文件, 如果希望抽离成分别不同的css, 需要再引入MiniCssExtractPlugin,再配置
'css-loader', // css-loader 用来解析@import这种语法
'postcss-loader',
'less-loader' // less-loader less -> css
// sass node-sass sass-loader
// stylus stylus-loader
]
}
less-loader
抽离css文件,通过link引入
yarn add mini-css-extract-plugin -D
mini-css-extract-plugin
let MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 压缩css
plugins: [
new MiniCssExtractPlugin({
filename: 'css/main.css'
})
]
{
test: /.css$/, // css 处理
// use: 'css-loader'
// use: ['style-loader', 'css-loader'],
use: [
// {
// loader: 'style-loader',
// options: {
// insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖
// }
// },
// 此时不需要style-loader
MiniCssExtractPlugin.loader, // 抽离
'css-loader', // css-loader 用来解析@import这种语法,
'postcss-loader'
]
}
抽离css插件文件时可使用optimize-css-assets-webpack-plugin优化压缩css以及js文件
压缩css和js
// 用了mini-css-extract-plugin
抽离css为link需使用optimize-css-assets-webpack-plugin
进行压缩css,使用此方法压缩了css需要uglifyjs-webpack-plugin
压缩js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
const UglifyJsPlugin = require("uglifyjs-webpack-plugin")
module.exports = {
optimization: { // 优化项
minimizer: [
new UglifyJsPlugin({ // 优化js
cache: true, // 是否缓存
parallel: true, // 是否并发打包
// sourceMap: true // 源码映射 set to true if you want JS source maps
}),
new OptimizeCSSAssetsPlugin({}) // css 的优化
]
},
mode: 'production',
entry: '',
output: {},
}
给css加上兼容浏览器的前缀
yarn add postcss-loader autoprefixer -D
// css
{
test: /.css$/, // css 处理
// use: 'css-loader'
// use: ['style-loader', 'css-loader'],
use: [
// {
// loader: 'style-loader',
// options: {
// insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖
// }
// },
MiniCssExtractPlugin.loader,
'css-loader', // css-loader 用来解析@import这种语法,
'postcss-loader'
]
}
// less
{
test: /.less$/, // less 处理
// use: 'css-loader'
// use: ['style-loader', 'css-loader'],
use: [
// {
// loader: 'style-loader',
// options: {
// insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖
// }
// },
MiniCssExtractPlugin.loader, // 这样相当于抽离成一个css文件, 如果希望抽离成分别不同的css, 需要再引入MiniCssExtractPlugin,再配置
'css-loader', // css-loader 用来解析@import这种语法
'postcss-loader',
'less-loader' // less-loader less -> css
// sass node-sass sass-loader
// stylus stylus-loader
]
},
postcss 需要配置文档 postcss.config1.js
postcss-loader
module.exports = {
plugins: [
require('autoprefixer')
]
}
es6 转 es5
npm i babel-loader @babel/core @babel/preset-env -D
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [ //预设
'@babel/preset-env'
],
plugins:[
// 转es7的语法
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
},
exclude: /node_modules/
}
]
}
}
转es7的语法
// 转class
npm i @babel/plugin-proposal-class-properties -D
// 转装饰器
npm i @babel/plugin-proposal-decorators -D
配置如上
其他不兼容的高级语法
使用 @babel/polyfill
语法检查 eslint
npm i eslint eslint-loader -S
根目录添加 .eslintrc.json 配置文件
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'eslint-loader',
options: {
enforce: 'pre' // previous优先执行 post-普通loader之后执行
}
}
},
{
test: /.js$/, // mormal 普通的loader
use: {
loader: 'babel-loader',
options: {
presets: [ //预设
'@babel/preset-env'
]
}
},
exclude: /node_modules/
}
]
}
}
全局变量引入
jquery的引入
npm i jquery -S
let webpack = require('webpack')
new webpack.ProvidePlugin({
$: 'jquery'
})
其他情况
暴露全局
npm i expose-loader -D 暴露全局的loader
法1:
可以在js中 import $ from 'expose-loader?$!jquery' // 全局暴露jquery为$符号
可以调用window.$
法2:
也可在webpack.config.js 中配置 rules
module.exports = {
module: {
rules: [
{
test: require.resolve('jquery'),
use: 'expose-loader?$'
}
]
}
}
以后在.js文件中引入
import $ from 'jquery'
法3. 如何在每个模块中注入:
let webpack = require('webpack')
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery'
})
]
}
之后代码内直接使用 $
法4:
在index.html中通过script标签引入jquery, 但是在js中,用import会重新打包jquery,如何避免
从输出的bundle 中排除依赖
module.exports = {
externals: { // 告知webpack是外部引入的,不需要打包
jquery: 'jQuery'
}
}
此时在index.js上
import $ from 'jquery'
console.log($)
webpack图片打包
js中创建
css中引入
yarn add file-loader -D
适合一二情况
module.export={
module: {
rules: [
{
test: /.(png|jpg|gif)$/,
use: 'file-loader'
}
]
}
}
默认会内部生成一张图片到build,生成图片的路径返回回来
第一种情况: 图片地址要import引入,直接写图片的地址,会默认为字符串
import logo from './logo.png'
let image = new Image()
image.src = logo
document.body.appendChild(image)
第二种情况: css-loader会将css里面的图片转为require的格式
div {
background: url("./logo.png");
}
第三种情况: 解析html中的image
yarn add html-withimg-loader -D
{
test: /.html$/,
use: 'html-withimg-loader'
}
当图片小于多少,用base64
yarn add url-loader -D
如果过大,才用file-loader
{
test: /.(png|jpg|gif)$/,
// 当图片小于多少,用base64,否则用file-loader产生真实的图片
use: {
loader: 'url-loader',
options: {
limit: 200 * 1024, // 小于200k变成base64
// outputPath: '/img/', // 打包后输出地址
// publicPath: '' // 给资源加上域名路径
}
}
}
打包文件分类
1.图片:
{
test: /.(png|jpg|gif)$/,
// 当图片小于多少,用base64,否则用file-loader产生真实的图片
use: {
loader: 'url-loader',
options: {
limit: 1, // 200k 200 * 1024
outputPath: 'img/' // 打包后输出地址 在dist/img
}
}
},
2.css:
plugins: [
new MiniCssExtractPlugin({
filename: 'css/main.css'
}),
]
希望输出的时候,给这些cssimg加上前缀,传到服务器也能访问
output: {
filename: 'bundle.[hash:8].js', // hash: 8只显示8位
path: path.resolve(__dirname, 'dist'),
publicPath: 'http://www.mayufo.cn' // 给静态资源统一加
},
如果只希望处理图片
{
test: /.(png|jpg|gif)$/,
// 当图片小于多少,用base64,否则用file-loader产生真实的图片
use: {
loader: 'url-loader',
options: {
limit: 1, // 200k 200 * 1024
outputPath: '/img/', // 打包后输出地址
publicPath: 'http://www.mayufo.cn'
}
}
}
打包多页应用
// 多入口
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
home: './src/index.js',
other: './src/other.js'
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, 'dist2')
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
filename: 'home.html',
chunks: ['home']
}),
new HtmlWebpackPlugin({
template: './index.html',
filename: 'other.html',
chunks: ['other', 'home'] // other.html 里面有 other.js & home.js
}),
]
}
配置source-map
yarn add @babel/core @babel/preset-env babel-loader webpack-dev-server -D
module.exports = {
devtool: 'source-map' // 增加映射文件调试源代码
}
源码映射 会标识错误的代码 打包后生成独立的文件 大而全 「source-map」
不会陈胜单独的文件 但是可以显示行和列 「eval-source-map」
不会产生列有行,产生单独的映射文件 「cheap-module-source-map」
不会产生文件 集成在打包后的文件中 不会产生列有行 「cheap-module-eval-source-map」
watch 改完代表重新打包实体
module.exports = {
watch: true,
watchOptions: {
poll: 1000, // 每秒监听1000次
aggregateTimeout: 300, // 防抖,当第一个文件更改,会在重新构建前增加延迟
ignored: /node_modules/ // 对于某些系统,监听大量文件系统会导致大量的 CPU 或内存占用。这个选项可以排除一些巨大的文件夹,
},
}
webpack的其他三个小插件
cleanWebpackPlugin
每次打包之前删掉dist目录 yarn add clean-webpack-plugin -D
clean-webpack-plugin
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
output: {
path: path.resolve(process.cwd(), 'dist'),
},
plugins: [
new CleanWebpackPlugin('./dist')
]
}
copyWebpackPlugin
一些静态资源也希望拷贝的dist中
yarn add copy-webpack-plugin -D
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin([
{from: 'doc', to: './dist'}
])
]
}
bannerPlugin内置模块
版权声明
const webpack = require('webpack');
new webpack.BannerPlugin('hello world')
// or
new webpack.BannerPlugin({ banner: 'hello world'})
webpack 跨域
设置一个服务,由于webpack-dev-server内含express
express
server.js
// express
let express = require('express')
let app = express();
app.get('/api/user', (res) => {
res.json({name: 'mayufo'})
})
app.listen(3000) // 服务端口在3000
写完后记得node server.js
访问 http://localhost:3000/api/user 可见内容
index.js
// 发送一个请求
let xhr = new XMLHttpRequest();
// 默认访问 http://localhost:8080 webpack-dev-server 的服务 再转发给3000
xhr.open('GET', '/api/user', true);
xhr.onload = function () {
console.log(xhr.response)
}
xhr.send();
webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
},
}
1.如果后端给的请求没有API 「跨域」
// express
let express = require('express')
let app = express();
app.get('/user', (res) => {
res.json({name: 'mayufo'})
})
app.listen(3000) // 服务端口在3000
请求已api开头, 转发的时候再删掉api
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api': ''}
}
}
}
2.前端只想单纯mock数据 「跨域」
devServer: {
// proxy: {
// '/api': 'http://localhost:3000' // 配置一个代理
// }
// proxy: { // 重写方式 把请求代理到express 上
// '/api': {
// target: 'http://localhost:3000',
// pathRewrite: {'^/api': ''}
// }
// }
before: function (app) { // 勾子
app.get('/api/user', (req, res) => {
res.json({name: 'tigerHee'})
})
}
},
3.有服务端,不用代理, 服务端启动webpack 「跨域」
server.js中启动webpack
yarn add webpack-dev-middleware -D
server.js
// express
let express = require('express')
let webpack = require('webpack')
let app = express();
// 中间件
let middle = require('webpack-dev-middleware')
let config = require('./webpack.config')
let compiler = webpack(config)
app.use(middle(compiler))
app.get('/user', (req, res) => {
res.json({name: 'mayufo'})
})
app.listen(3000)
webpack解析resolve
以bootstrap为例
npm install bootstrap -D
index.js
import 'bootstrap/dist/css/bootstrap.css'
报错
ERROR in ./node_modules/bootstrap/dist/css/bootstrap.css 7:0
Module parse failed: Unexpected token (7:0)
You may need an appropriate loader to handle this file type.
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
| */
:root {
| --blue: #007bff;
| --indigo: #6610f2;
@ ./src/index.js 22:0-42
@ multi (webpack)-dev-server/client?http://localhost:8081 ./src/index.js
这是因为bootstrap 4.0的css引入了新的特性,CSS Variables
安装 npm install postcss-custom-properties --save-dev
配置webpack.config.js
{
test: /.css$/,
use: ['style-loader', 'css-loader', {
loader: 'postcss-loader',
options: {
plugins: (loader) => [
require("postcss-custom-properties")
]
}
}]
}
但是每次引入都很长,如何优雅引入
resolve: {
// 在当前目录查找
modules: [path.resolve('node_modules')],
alias: {
'bootstrapCss': 'bootstrap/dist/css/bootstrap.css'
}
},
import 'bootstrapCss' // 在node_modules查找
省略扩展名
extensions:
resolve: {
// 在当前目录查找
modules: [path.resolve('node_modules')],
// alias: {
// 'bootstrapCss': 'bootstrap/dist/css/bootstrap.css'
// },
mainFields: ['style', 'main'], // 先用bootstrap中在package中的style,没有在用main
// mainFiles: [] // 入口文件的名字 默认index
extensions: ['.js', '.css', '.json'] // 当没有拓展命的时候,先默认js、次之css、再次之json
},
定义环境变量
DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和生产模式的构建允许不同的行为非常有用。
let url = ''
if (DEV === 'dev') {
// 开发环境
url = 'http://localhost:3000'
} else {
// 生成环境
url = 'http://www.mayufo.cn'
}
webpack.config.js
new webpack.DefinePlugin({
// DEV: '"production"',
DEV: JSON.stringify('production'),
FLAG: 'true', // 布尔
EXPRESSION: '1 + 1' // 字符串 如果希望是字符串 JSON.stringify('1 + 1')
})
区分两个不同的环境
分别配置不同的环境
webpack.base4.js 基础配置
webpack.dev4.js 开发环境
webpack.prod4.js 生产环境
yarn add webpack-merge -D
npm run build -- -- config webpack.dev4.js npm run build -- -- config webpack.build.js
官方文档
webpack.base4.js
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
let CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
entry: {
home: './src/index.js'
},
output: {
filename: "[name].js",
path: path.resolve(process.cwd(), 'dist3')
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
},
{
test: /.css$/,
use: ['style-loader', 'css-loader', {
loader: 'postcss-loader',
options: {
plugins: (loader) => [
require("postcss-custom-properties")
]
}
}]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
})
]
}
webpack.dev4.js
let merge = require('webpack-merge')
let base = require('./webpack.base4.js')
module.exports = merge(base, {
mode: 'development',
devServer: {},
devtool: 'source-map'
})
webpack.prod4.js
let merge = require('webpack-merge')
let base = require('./webpack.base4.js')
module.exports = merge(base, {
mode: 'production'
})
package.json
"scripts": {
"build": "webpack --config webpack.prod4.js",
"dev": "webpack-dev-server --config webpack.dev4.js"
},
webpack 优化
yarn add webpack webpack-cli html-webpack-plugin @babel/core babel-loader @babel/preset-env @babel/preset-react -D
webpack.config.js
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
]
}
优化:当某些包是独立的个体没有依赖
以jquery为例,yarn add jquery -D,它是一个独立的包没有依赖,可以在webpack配置中,配置它不再查找依赖
module: {
noParse: /jquery/, // 不用解析某些包的依赖
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}
},
]
}
运行npx webpack
从2057ms -> 1946 ms
优化:规则匹配设置范围
rules: [
{
test: /.js$/,
exclude: '/node_modules/', // 排除
include: path.resolve('src'), // 在这个范围内
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}
}
尽量实用include,不使用exclude,使用绝对路径
优化:忽略依赖中不必要的语言包
yarn add moment webpack-dev-server -D
忽略掉moment的其他语言包
let webpack = require('webpack')
plugins: [
new webpack.IgnorePlugin(/./locale/, /moment/)
]
index.js
import moment from 'moment'
let r = moment().endOf('day').fromNow() // 距离现在多少天
console.log(r);
从 1.2MB 到 800kb
动态链接库
yarn add react react-dom
正常使用
webpack.config.js
{
test: /.js$/,
exclude: '/node_modules/',
include: path.resolve('src'),
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}
}
index.js
import React from 'react'
import {render} from 'react-dom'
render(
111111
, window.root)index.html 独立的将react react-dom 打包好, 打包好再引用,从而减少webpack每次都要打包react
创建webpack.config.react.js
let path = require('path')
let webpack = require('webpack')
module.exports = {
mode: 'development',
entry: {
// test: './src/test.js'
react: ['react', 'react-dom']
},
output: {
filename: 'dll[name].js', // 产生的文件名
path: path.resolve(__dirname, 'dist'),
library: 'dll[name]', // 给输出的结果加个名字
// libraryTarget: 'var' // 配置如何暴露 library
// commonjs 结果放在export属性上, umd统一资源模块, 默认是var
},
plugins: [
new webpack.DllPlugin({
name: 'dll[name]', // name === library
path: path.resolve(__dirname, 'dist', 'manifest.json') // manifest.json 定义了各个模块的路径
})
]
}
libraryTarget
manifest.json就是一个任务清单or动态链接库,在这个清单里面查找react
npx webpack --config webpack.config.react.js
在index.html增加引用
在webpack.config.js 中配置,现在动态链接库manifest.json中查找,如果没有再打包reactplugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dist', 'manifest.json')
})
]
DLLPlugin 和 DLLReferencePlugin
npm run build
打包后的bunle.js文件变小
npm run dev
可以理解为先把react打包,后面每次都直接使用react打包后的结果
多线程打包happypack
yarn add happypack
webpack.config.js
let Happypack = require('happypack')
rules: [
{
test: /.js$/,
exclude: '/node_modules/',
include: path.resolve('src'),
use: 'happypack/loader?id=js'
},
]
plugins: [
new Happypack({
id: 'js',
use: [{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}]
})
]
js启用多线程,由于启用多线程也会浪费时间,因此当项目比较大的时候启用效果更好
css启用多线程
{
test: /.css$/,
use: 'happypack/loader?id=css'
}
new Happypack({
id: 'css',
use: ['style-loader', 'css-loader']
}),
webpack 自带的优化
test.js
let sum = (a, b) => {
return a + b + 'sum'
}
let minus = (a, b) => {
return a - b + 'minus';
}
export default {
sum, minus
}
使用import
index.js
import calc from './test'
console.log(calc.sum(1, 2));
import在生产环境下会自动去除没有用的代码minus,这叫tree-shaking,将没有用的代码自动删除掉
index.js
let calc = require('./test')
console.log(calc); // es 6导出,是一个default的对象
console.log(calc.default.sum(1, 2));
require引入es6 模块会把结果放在default上,打包build后并不会把多余minus代码删除掉,不支持tree-shaking
作用域的提升
index.js
let a = 1
let b = 2
let c = 3
let d = a + b + c
console.log(d, '---------');
打包出来的文件
console.log(r.default.sum(1,2));console.log(6,"---------")
在webpack中可以省略一些可以简化的代码
抽取公共代码
抽离自有模块
webpack.config.js
module.exports = {
optimization: {
splitChunks: { // 分割代码块,针对多入口
cacheGroups: { // 缓存组
common: { // 公共模块
minSize: 0, // 大于多少抽离
minChunks: 2, // 使用多少次以上抽离抽离
chunks: 'initial' // 从什么地方开始, 从入口开始
}
}
}
},
}
SplitChunksPlugin
分别有a.js和b.js, index.js和other.js分别引入a和b两个js
index.js
import './a'
import './b'
console.log('index.js');
other.js
import './a'
import './b'
console.log('other.js');
webpack.config.js
module.exports = {
optimization: {
splitChunks: { // 分割代码块,针对多入口
cacheGroups: { // 缓存组
common: { // 公共模块
minSize: 0, // 大于多少抽离
minChunks: 2, // 使用多少次以上抽离抽离
chunks: 'initial' // 从什么地方开始, 从入口开始
}
}
}
},
}
抽离第三方模块
比如jquery
index.js 和 other.js分别引入
import $ from 'jquery'
console.log($);
修改webpack.config.js配置:
optimization: {
splitChunks: { // 分割代码块,针对多入口
cacheGroups: { // 缓存组
common: { // 公共模块
minSize: 0, // 大于多少抽离
minChunks: 2, // 使用多少次以上抽离抽离
chunks: 'initial' // 从什么地方开始,刚开始
},
vendor: {
priority: 1, // 增加权重, (先抽离第三方)
test: /node_modules/, // 把此目录下的抽离
minSize: 0, // 大于多少抽离
minChunks: 2, // 使用多少次以上抽离抽离
chunks: 'initial' // 从什么地方开始,刚开始
}
}
},
},
懒加载(延迟加载)
yarn add @babel/plugin-syntax-dynamic-import -D
source.js
export default 'mayufo'
index.js
let button = document.createElement('button')
button.innerHTML = 'hello'
button.addEventListener('click', function () {
console.log('click')
// es6草案中的语法,jsonp实现动态加载文件
import('./source.js').then(data => {
console.log(data.default)
})
})
document.body.appendChild(button)
webpack.config.js
{
test: /.js$/,
exclude: '/node_modules/',
include: path.resolve('src'),
use: [{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
'@babel/plugin-syntax-dynamic-import'
]
}
}]
}
热更新(当页面改变只更新改变的部分,不重新打包)
webpack.config.js
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new webpack.NameModulesPlugin(), // 打印更新的模块路径
new webpack.HotModuleReplacementPlugin() // 热更新插件
]
index.js
import str from './source'
console.log(str);
if (module.hot) {
module.hot.accept('./source', () => {
console.log('文件更新了');
require('./source')
console.log(str);
})
}
tapable介绍 - SyncHook
tapable
webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。
SyncHook 不关心监听函数的返回值
yarn add tabable
1.use.js
let {SyncHook} = require('tapable') // 结构同步勾子
class Lesson {
constructor () {
this.hooks = {
// 订阅勾子
arch: new SyncHook(['name']),
}
}
start () {
this.hooks.arch.call('may')
}
tap () { // 注册监听函数
this.hooks.arch.tap('node', function (name) {
console.log('node', name)
})
this.hooks.arch.tap('react', function (name) {
console.log('react', name)
})
}
}
let l = new Lesson()
l.tap(); //注册两个函数
l.start() // 启动勾子
1.theory.js
class SyncHook { // 勾子是同步的
constructor(args) { // args => ['name']
this.tasks = []
}
tap (name, task) {
this.tasks.push(task)
}
call (...args) {
this.tasks.forEach((task) => task(...args))
}
}
let hook = new SyncHook(['name'])
hook.tap('react', function (name) {
console.log('react', name);
})
hook.tap('node', function (name) {
console.log('node', name);
})
hook.call('jw')
tapable介绍 - SyncBailHook
SyncBailHook为勾子加了个保险,当return返回不是undefine就会停止
2.use.js
let {SyncBailHook} = require('tapable') // 解构同步勾子
class Lesson {
constructor () {
this.hooks = {
// 订阅勾子
arch: new SyncBailHook(['name']),
}
}
start () {
// 发布
this.hooks.arch.call('may')
}
tap () { // 注册监听函数,订阅
this.hooks.arch.tap('node', function (name) {
console.log('node', name)
return '停止学习' // 会停止
// return undefined
})
this.hooks.arch.tap('react', function (name) {
console.log('react', name)
})
}
}
let l = new Lesson()
l.tap(); //注册两个函数
l.start() // 启动勾子
2.theory.js
class SyncBailHook { // 勾子是同步的
constructor(args) { // args => ['name']
this.tasks = []
}
tap (name, task) {
this.tasks.push(task)
}
call (...args) {
let ret; // 当前函数的返回值
let index = 0; // 当前要执行的第一个
do {
ret = this.tasksindex
} while (ret === undefined && index < this.tasks.length)
}
}
let hook = new SyncBailHook(['name'])
hook.tap('react', function (name) {
console.log('react', name);
return '停止学习'
// return undefined
})
hook.tap('node', function (name) {
console.log('node', name);
})
hook.call('jw')
tapable介绍 - SyncWaterfallHook
SyncWaterfallHook上一个监听函数的返回值可以传给下一个监听函数
3.use.js
let {SyncWaterfallHook} = require('tapable') // 解构同步勾子
// waterfall 瀑布
class Lesson {
constructor () {
this.hooks = {
// 订阅勾子
arch: new SyncWaterfallHook(['name']),
}
}
start () {
// 发布
this.hooks.arch.call('may')
}
tap () { // 注册监听函数,订阅
this.hooks.arch.tap('node', function (name) {
console.log('node', name)
return '学的不错'
})
this.hooks.arch.tap('react', function (name) {
console.log('react', name)
})
}
}
let l = new Lesson()
l.tap(); //注册两个函数
l.start() // 启动勾子
3.theory.js
class SyncWaterfallHook { // 勾子是同步的 - 瀑布
constructor(args) { // args => ['name']
this.tasks = []
}
tap (name, task) {
this.tasks.push(task)
}
call (...args) {
let [first, ...others] = this.tasks;
let ret = first(...args)
others.reduce((a, b) => {
return b(a);
}, ret);
}
}
let hook = new SyncWaterfallHook(['name'])
hook.tap('react', function (name) {
console.log('react', name);
return 'react Ok'
// return undefined
})
hook.tap('node', function (name) {
console.log('node', name);
return 'node Ok'
})
hook.tap('webpack', function (data) {
console.log('webpack', data);
})
hook.call('jw')
tapable介绍 - SyncLoopHook
SyncLoopHook当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
4.use.js
let {SyncLoopHook} = require('tapable') // 解构同步勾子
// 不返回undefined 会多次执行
class Lesson {
constructor () {
this.index = 0
this.hooks = {
// 订阅勾子
arch: new SyncLoopHook(['name']),
}
}
start () {
// 发布
this.hooks.arch.call('may')
}
tap () { // 注册监听函数,订阅
this.hooks.arch.tap('node', (name) => {
console.log('node', name)
return ++this.index === 3 ? undefined : '继续学'
})
this.hooks.arch.tap('react', (name) => {
console.log('react', name)
})
}
}
let l = new Lesson()
l.tap(); //注册两个函数
l.start() // 启动勾子
4.theory.js
class SyncLoopHook { // 勾子是同步的 - 瀑布
constructor(args) { // args => ['name']
this.tasks = []
}
tap (name, task) {
this.tasks.push(task)
}
call (...args) {
this.tasks.forEach(task => {
let ret
do {
ret = task(...args);
} while(ret !== undefined)
})
}
}
let hook = new SyncLoopHook(['name'])
let total = 0
hook.tap('react', function (name) {
console.log('react', name);
return ++total === 3 ? undefined: '继续学'
})
hook.tap('node', function (name) {
console.log('node', name);
})
hook.tap('webpack', function (data) {
console.log('webpack', data);
})
hook.call('jw')
AsyncParallelHook 与 AsyncParallelBailHook
异步的勾子分两种串行和并行
并行等待所有并发的异步事件执行后执行回调
注册的三种方法
异步的注册方法tap
异步的注册方法tapAsync, 还有个回调参数
topPromise,注册promise
调用的三种
call (同步)
callAsync (异步)
promise (异步)
这里介绍的是异步并行的
AsyncParallelHook
不关心监听函数的返回值。
5.use.js
let {AsyncParallelHook} = require('tapable') // 解构同步勾子
// 不返回undefined 会多次执行
class Lesson {
constructor() {
this.index = 0
this.hooks = {
// 订阅勾子
arch: new AsyncParallelHook(['name']),
}
}
start() {
// 发布callAsync
// this.hooks.arch.callAsync('may', function () {
// console.log('end');
// })
// 另一种发布promise
this.hooks.arch.promise('may').then(function () {
console.log('end');
}
)
}
tap() { // 注册监听函数,订阅
// 注册tapAsync
// this.hooks.arch.tapAsync('node', (name, callback) => {
// setTimeout(() => {
// console.log('node', name)
// callback()
// }, 1000)
// })
// this.hooks.arch.tapAsync('react', (name, callback) => {
// setTimeout(() => {
// console.log('react', name)
// callback()
// }, 1000)
// })
// 另一种订阅 tapPromise
this.hooks.arch.tapPromise('node', (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('node', name)
resolve()
}, 1000)
})
})
this.hooks.arch.tapPromise('react', (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('react', name)
resolve()
}, 1000)
})
})
}
}
let l = new Lesson()
l.tap(); //注册两个函数
l.start() // 启动勾子
5.theory.js
class AsyncParallelHook { // 勾子是同步的 - 瀑布
constructor(args) { // args => ['name']
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
tapPromise(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
let finalCallback = args.pop() // 拿出最终的函数
let index = 0
let done = () => { // 类似promise.all的实现
index++;
if (index === this.tasks.length) {
finalCallback();
}
}
this.tasks.forEach(task => {
task(...args, done) // 这里的args 已经把最后一个参数删掉
})
}
promise(...args) {
let tasks = this.tasks.map(task => task(...args))
return Promise.all(tasks)
}
}
let hook = new AsyncParallelHook(['name'])
// hook.tapAsync('react', function (name, callback) {
// setTimeout(() => {
// console.log('react', name);
// callback()
// }, 1000)
// })
//
// hook.tapAsync('node', function (name, callback) {
// setTimeout(() => {
// console.log('node', name);
// callback()
// }, 1000)
// })
// hook.tapAsync('webpack', function (name, callback) {
// setTimeout(() => {
// console.log('webpack', name);
// callback()
// }, 1000)
// })
hook.tapPromise('react', function (name, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('react', name);
resolve()
}, 1000)
})
})
hook.tapPromise('node', function (name, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('node', name);
resolve()
}, 1000)
})
})
//
// hook.callAsync('jw', function () {
// console.log('end');
// })
hook.promise('jw').then(function () {
console.log('end');
})
AsyncParallelBailHook
只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数。
使用和原理与SyncBailHook相似
异步串行 —— AsyncSeriesHook
串行 one by one
6.use.js
let {AsyncSeriesHook} = require('tapable') // 解构同步勾子
class Lesson {
constructor() {
this.index = 0
this.hooks = {
// 订阅勾子
arch: new AsyncSeriesHook(['name']),
}
}
start() {
// 发布
// this.hooks.arch.callAsync('may', function () {
// console.log('end');
// })
// 另一种发布
this.hooks.arch.promise('may').then(function () {
console.log('end');
}
)
}
tap() { // 注册监听函数,订阅
// this.hooks.arch.tapAsync('node', (name, callback) => {
// setTimeout(() => {
// console.log('node', name)
// callback()
// }, 1000)
// })
// this.hooks.arch.tapAsync('react', (name, callback) => {
// setTimeout(() => {
// console.log('react', name)
// callback()
// }, 1000)
// })
// 另一种订阅
this.hooks.arch.tapPromise('node', (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('node', name)
resolve()
}, 1000)
})
})
this.hooks.arch.tapPromise('react', (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('react', name)
resolve()
}, 1000)
})
})
}
}
let l = new Lesson()
l.tap(); //注册两个函数
l.start(); // 启动勾子
6.theory.js
class AsyncSeriesHook { //
constructor(args) { // args => ['name']
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
tapPromise(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
let finalCallback = args.pop()
let index = 0;
let next = () => {
if (this.tasks.length === index) return finalCallback();
let task = this.tasks[index++];
task(...args, next);
}
next();
}
promise(...args) {
// 将promise串联起来
let [first, ...other] = this.tasks
return other.reduce((p, n) => {
return p.then(() => n (...args))
}, first(...args))
}
}
let hook = new AsyncSeriesHook(['name'])
// hook.tapAsync('react', function (name, callback) {
// setTimeout(() => {
// console.log('react', name);
// callback()
// }, 1000)
// })
//
// hook.tapAsync('node', function (name, callback) {
// setTimeout(() => {
// console.log('node', name);
// callback()
// }, 1000)
// })
//
// hook.tapAsync('webpack', function (name, callback) {
// setTimeout(() => {
// console.log('webpack', name);
// callback()
// }, 1000)
// })
hook.tapPromise('react', function (name, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('react', name);
resolve()
}, 1000)
})
})
hook.tapPromise('node', function (name, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('node', name);
resolve()
}, 1000)
})
})
// hook.callAsync('jw', function () {
// console.log('end');
// })
hook.promise('jw').then(function () {
console.log('end');
})
异步串行 —— AsyncSeriesWaterfallHook
上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数
7.use.js
let {AsyncSeriesWaterfallHook} = require('tapable') // 解构同步勾子
class Lesson {
constructor() {
this.index = 0
this.hooks = {
// 订阅勾子
arch: new AsyncSeriesWaterfallHook(['name']),
}
}
start() {
// 发布
this.hooks.arch.callAsync('may', function () {
console.log('end');
})
// 另一种发布
// this.hooks.arch.promise('may').then(function () {
// console.log('end');
// }
// )
}
tap() { // 注册监听函数,订阅
this.hooks.arch.tapAsync('node', (name, callback) => {
setTimeout(() => {
console.log('node', name)
// callback(null, 'result')
callback('error', 'result') // 如果放error, 会跳过直接后面的勾子,直接走到最终的
}, 1000)
})
this.hooks.arch.tapAsync('react', (name, callback) => {
setTimeout(() => {
console.log('react', name)
callback()
}, 1000)
})
// 另一种订阅
// this.hooks.arch.tapPromise('node', (name) => {
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// console.log('node', name)
// resolve()
// }, 1000)
// })
// })
// this.hooks.arch.tapPromise('react', (name) => {
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// console.log('react', name)
// resolve()
// }, 1000)
// })
// })
}
}
let l = new Lesson()
l.tap(); //注册两个函数
l.start(); // 启动勾子
7.theory.js
class AsyncSeriesWaterfallHook { //
constructor(args) { // args => ['name']
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
tapPromise(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
let finalCallback = args.pop()
let index = 0;
let next = (err, data) => {
let task = this.tasks[index]
if(!task) return finalCallback();
if (index === 0) {
// 执行的第一个
task(...args, next)
} else {
task(data, next)
}
index ++
}
next();
}
promise(...args) {
// 将promise串联起来
let [first, ...other] = this.tasks
return other.reduce((p, n) => {
return p.then((data) => n(data))
}, first(...args))
}
}
let hook = new AsyncSeriesWaterfallHook(['name'])
// hook.tapAsync('react', function (name, callback) {
// setTimeout(() => {
// console.log('react', name);
// callback(null, '结果1')
// }, 1000)
// })
//
// hook.tapAsync('node', function (name, callback) {
// setTimeout(() => {
// console.log('node', name);
// callback(null, '结果2')
// }, 1000)
// })
//
// hook.tapAsync('webpack', function (name, callback) {
// setTimeout(() => {
// console.log('webpack', name);
// callback()
// }, 1000)
// })
//
hook.tapPromise('react', function (name, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('react', name);
resolve('result')
}, 1000)
})
})
hook.tapPromise('node', function (name, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('node', name);
resolve()
}, 1000)
})
})
//
//
// hook.callAsync('jw', function () {
// console.log('end');
// })
hook.promise('jw').then(function () {
console.log('end');
})
手写webpack
对应的may-pack项目
yarn add webpack webpack-cli -D
webpack.config.js
let path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
npx webpack
生成文件bundle.js
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/a.js":
(function (module, exports, webpack_require) {
eval("let b = webpack_require(/*! ./base/b / "./src/base/b.js")
module.exports = 'a'+ b
//# sourceURL=webpack:///./src/a.js?");
}),
"./src/base/b.js":
(function (module, exports) {
eval("module.exports = 'b'
//# sourceURL=webpack:///./src/base/b.js?");
}),
"./src/index.js":
(function (module, exports, webpack_require) {
eval(" let str = webpack_require(/! ./a.js */ "./src/a.js")
console.log(str);
//# sourceURL=webpack:///./src/index.js?");
})
});
新建项目用于自己的webpack,这里叫may-pack
yarn init
如果在node里想执行命令,创建bin文件,再创建may-pack.js
配置package.json
{
"name": "may-pack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"bin": {
"may-pack": "./bin/may-pack.js"
}
}
may-pack.js
! /usr/bin/env node
// node环境
console.log('start');
运行npm link将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试
在想运行may-pack的项目中运行,npm link may-pack 得到 start
webpack分析及处理
may-pack.js
! /usr/bin/env node
// node环境
console.log('start');
let path = require('path')
// 拿到配置文件webpack.config.js
let config = require(path.resolve('webpack.config.js'));
let Compiler = require('../lib/Compiler.js');
let compiler = new Compiler(config);
// 标识运行编译
compiler.run()
创建lib文件Compiler.js
let path = require('path')
let fs = require('fs')
class Compiler {
constructor(config) {
// entry output
this.config = config
// 需要保存入口文件的路径
this.entryId = ''; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry // 入口文件
// 工作目录
this.root = process.cwd(); // 当前运行npx的路径
}
// 构建模块
buildModule(modulePath, isEntry) {
}
// 发射文件
emitFile() {
// 用数据 渲染想要的
}
run() {
// 执行 创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径
// 发射打包后的文件
this.emitFile()
}
}
module.exports = Compiler
主要两个任务
拿到入口Id
解析模块,也就是实现buildModule方法
创建依赖关系
may-pack中Compiler.js
let path = require('path')
let fs = require('fs')
// babylon 主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点
// @babel/types 用于AST节点的Lodash-esque实用程序库
// @babel/generator 结果生成
let babylon = require('babylon')
let traverse = require('@babel/traverse').default;
let type = require('@babel/types');
let generator = require('@babel/generator').default
class Compiler {
constructor(config) {
// entry output
this.config = config
// 需要保存入口文件的路径
this.entryId = ''; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry // 入口文件
// 工作目录
this.root = process.cwd(); // 当前运行npx的路径
}
// 拿到模块内容
getSource (modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
parse (source, parentPath) {
console.log(source, parentPath)
}
// 构建模块
buildModule(modulePath, isEntry) {
// 拿到模块内容
let source = this.getSource(modulePath) // 得到入口文件的内容
// 模块id modulePath(需要相对路径) = modulePath(模块路径) - this.root(项目工作路径) src/index.js
let moduleName = './' + path.relative(this.root, modulePath)
console.log(source, moduleName); // 拿到代码 和相对路径 ./src/index.js
if (isEntry) {
this.entryId = moduleName
}
let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)) // ./src
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode
}
// 发射文件
emitFile() {
// 用数据 渲染想要的
}
run() {
// 执行 创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径
console.log(this.modules, this.entryId);
// 发射打包后的文件
this.emitFile()
}
}
module.exports = Compiler
ast递归解析
parse方法主要靠解析语法树来进行转义 babylon 主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。 @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点 @babel/types 用于AST节点的Lodash-esque实用程序库 @babel/generator 结果生成
yarn add babylon @babel/traverse @babel/types @babel/generator
may-pack中Compiler.js
let path = require('path')
let fs = require('fs')
// babylon 主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点
// @babel/types 用于AST节点的Lodash-esque实用程序库
// @babel/generator 结果生成
let babylon = require('babylon')
let traverse = require('@babel/traverse').default;
let type = require('@babel/types');
let generator = require('@babel/generator').default
class Compiler {
constructor(config) {
// entry output
this.config = config
// 需要保存入口文件的路径
this.entryId = ''; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry // 入口文件
// 工作目录
this.root = process.cwd(); // 当前运行npx的路径
}
// 拿到模块内容
getSource (modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
parse (source, parentPath) {
// AST解析语法树
let ast = babylon.parse(source)
let dependencies = []; // 依赖的数组
// https://astexplorer.net/
traverse(ast, {
// 调用表达式
CallExpression(p) {
let node = p.node; //对应的节点
if(node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let moduledName = node.arguments[0].value // 取到模块的引用名字
moduledName = moduledName + (path.extname(moduledName) ? '': '.js'); // ./a.js
moduledName = './' + path.join(parentPath, moduledName) // './src/a.js'
dependencies.push(moduledName)
node.arguments = [type.stringLiteral(moduledName)] // 改掉源码
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
// 构建模块
buildModule(modulePath, isEntry) {
// 拿到模块内容
let source = this.getSource(modulePath) // 得到入口文件的内容
// 模块id modulePath(需要相对路径) = modulePath(模块路径) - this.root(项目工作路径) src/index.js
let moduleName = './' + path.relative(this.root, modulePath)
// console.log(source, moduleName); // 拿到代码 和相对路径 ./src/index.js
if (isEntry) {
this.entryId = moduleName
}
// 解析把source源码进行改造, 返回一个依赖列表
let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)) // ./src
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode
dependencies.forEach(dep => { // 附模块的加载 递归加载
this.buildModule(path.join(this.root, dep), false)
})
}
// 发射文件
emitFile() {
// 用数据 渲染想要的
}
run() {
// 执行 创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径
console.log(this.modules, this.entryId);
// 发射打包后的文件
this.emitFile()
}
}
module.exports = Compiler
生成打包工具
使用ejs模板
may-pack中main.ejs
(function (modules) {
var installedModules = {};
function webpack_require(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, webpack_require);
module.l = true;
return module.exports;
}
// Load entry module and return exports
return webpack_require(webpack_require.s = "<%-entryId %>");
})({
<% for(let key in modules){ %>
"<%- key %>":
(function (module, exports,webpack_require) {
eval(<%-modules[key] %>
);
}),
<% } %>
});
ejs入门
yarn add ejs
may-pack中Compiler.js
let ejs = require('ejs')
// 发射文件
emitFile() {
// 用数据 渲染想要的
// 输出到那个目录下
let main = path.join(this.config.output.path, this.config.output.filename)
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules})
this.assets = {}
// 路径对应的代码
this.assets[main] = code
fs.writeFileSync(main, this.assets[main])
}
在webpack-training项目中运行npx may-pack, 得到bundle.js,运行得到结果
增加loader
创建loader文件夹,创建less-loader1.js和style-loader1.js
yarn add less
less使用
less-loader1.js
// 将less转为css
let less = require('less')
function loader(source) {
let css = ''
less.render(source, function (err, output) {
css = output.css
})
css = css.replace(/
/g, '
');
return css
}
module.exports = loader
style-loader1.js
// 将css插入到html头部
function loader(source) {
console.log(111);
let style = let style = document.createElement('style') style.innerHTML = ${JSON.stringify(source)} document.head.appendChild(style)
return style
}
module.exports = loader
// JSON.stringify(source) 可以将代码转为一行
webpack.config.js
let path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.less$/,
use: [
path.resolve(__dirname, 'loader', 'style-loader1'),
path.resolve(__dirname, 'loader', 'less-loader1')
]
}
]
}
}
创建index.less
body {
background: red
}
index.js
let str = require('./a.js')
require('./index.less')
console.log(str);
may-pack中Compiler.js
// 拿到模块内容
getSource (modulePath) {
// 匹配各种文件的规则
let rules= this.config.module.rules; // webpack.config.js 中rules的数组
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) {
let rule = rules[i]
let {test, use} = rule
let len = use.length - 1
if (test.test(modulePath)) {
// console.log(use[len]);
function normalLoader () {
// console.log(use[len--]);
let loader = require(use[len--])
content = loader(content)
// 递归调用loader 实现转化
if (len >= 0) {
normalLoader()
}
}
normalLoader()
}
}
return content
}
运行npx may-pack
增加plugins
yarn add tapable
may-pack中Compiler.js
constructor(config) {
// entry output
this.config = config
// 需要保存入口文件的路径
this.entryId = ''; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry // 入口文件
// 工作目录
this.root = process.cwd(); // 当前运行npx的路径
this.hooks = {
entryOption: new SyncHook(), // 入口选项
compile: new SyncHook(), // 编译
afterCompile: new SyncHook(), // 编译完成
afterPlugins: new SyncHook(), // 编译完插件
run: new SyncHook(), // 运行
emit: new SyncHook(), // 发射
done: new SyncHook() // 完成
}
// 如果传递了plugins参数
let plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(plugin => {
plugin.apply(this); // 这里只是appLy方法不是改变this指向
})
}
this.hooks.afterPlugins.call()
}
在webpack.config.js中写插件方法
class P {
apply(compiler) { // 这里只是appLy方法不是改变this指向
// 绑定
compiler.hooks.emit.tap('emit', function () {
console.log('emit');
})
}
}
class P1 {
apply(compiler) { // 这里只是appLy方法不是改变this指向
// 绑定
compiler.hooks.afterPlugins.tap('emit', function () {
console.log('afterPlugins');
})
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.less$/,
use: [
path.resolve(__dirname, 'loader', 'style-loader'),
path.resolve(__dirname, 'loader', 'less-loader')
]
}
]
},
plugins: [
new P(),
new P1()
]
}
然后在各个地方调用
may-pack中may-pack.js
.....
// 调用
compiler.hooks.entryOption.call()
// 标识运行编译
compiler.run()
may-pack中Compiler.js
run() {
this.hooks.run.call()
this.hooks.compile.call()
// 执行 创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径
// console.log(this.modules, this.entryId);
this.hooks.afterCompile.call()
// 发射打包后的文件
this.emitFile()
this.hooks.emit.call()
this.hooks.done.call()
}
运行npx may-pack
loader
手写loader
webapck.config.js
let path = require('path')
module.exports = {
mode: 'development',
entry: './src/index',
output: {
filename: 'build.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.js/,
use: 'loader1' // 如何找到这个loader1
}
]
},
}
创建loader文件loader1.js
console.log(22);
function loader(source) { // loader的参数就是源代码
return source
}
module.exports = loader
webpack.config.js
let path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'build.js',
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
// 别名
// alias: {
// loader1: path.resolve(__dirname, 'loader', 'loader1')
// }
modules: ['node_modules', path.resolve(__dirname, 'loader')] // 先找node_modules, 再去loader中去找
},
module: {
rules: [
{
test: /.js$/,
// use: [path.resolve(__dirname, 'loader', 'loader1')]
use: 'loader1' // 如何找到这个loader1
},
// {
// test: /.less$/,
// use: [
// path.resolve(__dirname, 'loader', 'style-loader'),
// path.resolve(__dirname, 'loader', 'less-loader')
// ]
// }
]
},
}
如何找到这个loader1
通过配别名alias
通过modules
npx webpack
配置多个loader
数组方式
先分别在loader文件下创建,loader2.js和loader3.js
function loader(source) { // loader的参数就是源代码
console.log('loader2'); // loader3.js 类似
return source
}
module.exports = loader
webpack.config.js
rules: [
{
test: /.js$/,
use: ['loader3', 'loader2', 'loader1']
},
]
运行npx webpack,分别打出
loader1
loader2
loader3
对象方式
rules: [
{
test: /.js$/,
use: ['loader3']
},
{
test: /.js$/,
use: ['loader2']
},
{
test: /.js$/,
use: ['loader1']
}
]
运行npx webpack,分别打出
loader1
loader2
loader3
loader的顺序: 从右到左, 从下到上
也可以通过配置不同的参数改变loader的执行顺序,pre 前面的, post在后面的, normal正常
{
test: /.js$/,
use: ['loader1'],
enforce: "pre"
},
{
test: /.js$/,
use: ['loader2']
},
{
test: /.js$/,
use: ['loader3'],
enforce: "post"
},
loader 带参数执行的顺序: pre -> normal -> inline -> post
inline为行内loader
在loader文件中新建inlin-loader
function loader(source) { // loader的参数就是源代码
console.log('inline');
return source
}
module.exports = loader
src/a.js
module.exports = 'may'
src/index
console.log('hello')
let srt = require('-!inline-loader!./a')
-!禁用pre-loader和 normal-loader来处理了
loader1
loader2
loader3
inline
loader3
!禁用normal-loader
loader1
loader2
loader3
loader1
inline
loader3
!! 禁用pre-loader、normal-loader、post-loader,只能行内处理
loader1
loader2
loader3
inline
loader 默认由两部分组成pitch和normal
user: [loader3, loader2, loader1]
无返回值: 先执行pitch方法,从左到右,再获取资源
pitch loader - 无返回值
pitch loader3 → loader2 → loader1
↘
资源
↙
normal loader3 ← loader2 ← loader1
有返回值: 直接跳过后续所有的loader包括自己的,跳到之前的loader, 可用于阻断
loader
user: [loader3, loader2, loader1]
pitch loader - 有返回值
pitch loader3 → loader2 loader1
↙
有返回值 资源
↙
normal loader3 loader2 loader1
loadeer2.js
function loader(source) { // loader的参数就是源代码
console.log('loader2');
return source
}
loader.pitch = function () {
return '111'
}
module.exports = loader
结果
loader3
babel-loader实现
yarn add @babel/core @babel/preset-env
webpack.config.js
{
test: '.js$/',
use: {
loader: 'babel-loader2',
options: {
presets: [
'@babel/preset-env'
]
}
}
}
在loader文件创建babel-loader2.js(如果你已经装过babel-loader)
拿到babel的参数
yarn add loader-utils
// 需要在webpack.config.js拿到babel的预设, 通过预设转换模块, 先引入babel
let babel = require('@babel/core')
// 拿到babel的参数 需要工具 loaderUtils
let loaderUtils =require('loader-utils')
function loader(source) { // loader的参数就是源代码 这里的this就是loader的上下文
let options = loaderUtils.getOptions(this)
console.log(this.resourcePath, 444); // [./src/index.js]
let callback = this.async(); // babel的转换是异步的,同步的返回是不行的, 不能用return 同步就是直接掉用 异步会在async中
babel.transform(source, {
...options,
sourceMap: true, // 是否设置sourceMap 还需要再webpack.config.js 中配置 devtool: 'source-map'
filename: this.resourcePath.split('/').pop() // 给生成的source-map
指定名字
}, function (err, result) {
callback(err, result.code, result.map) // 异步 参数分别是「错误 转化后的代码 和 sourceMap」
})
console.log(options);
// return source 失效
}
module.exports = loader
index.js
class May {
constructor () {
this.name = 'may'
}
getName () {
return this.name
}
}
let may = new May()
console.log(may.getName());
npx webpack
banner-loader实现(自创)
给所有匹配的js加一个注释
webpack.config.js
{ // 给所有匹配的js
加一个注释
test: /.js$/,
use: {
loader: 'banner-loader',
options: {
text: 'may',
filename: path.resolve(__dirname, 'banner.js')
}
}
}
banner.js
二次星球中毒
在loader文件创建banner-loader.js
yarn add schema-utils 校验自己写的loader格式是否正确
schema-utils
banner-loader.js
// 拿到loader的配置
let loaderUtils = require('loader-utils')
// 校验loader
let validateOptions = require('schema-utils')
// 读取文件
let fs = require('fs') // 异步
function loader(source) { // loader的参数就是源代码
let options = loaderUtils.getOptions(this)
let callback = this.async() // 读取文件是异步
let schema = {
type: 'object',
properties: {
text: {
type: 'string'
},
filename: {
type: 'string'
}
}
}
validateOptions(schema, options, 'banner-loader') // 自己的校验格式, 自己的写的配置, 对应的loader名字
if (options.filename) {
this.cacheable(false) // 不要缓存 如果有大量计算 推荐缓存
// this.cacheable && this.cacheable()
this.addDependency(options.filename) // 自动增加依赖
fs.readFile(options.filename, 'utf8', function (err, data) {
callback(err, /**${data}**/${source}
)
})
} else {
callback(null, /**${options.text}**/${source}
)
}
return source
}
module.exports = loader
优化:
修改banner.js的内容后, webpack进行监控,打包webapck.config.js配置watch: true
loader缓存
实现file-loader和url-loader
yarn add mime
其主要用途是设置某种扩展名的文件的响应程序类型
mime
创建file-loader.js1
// 拿到babel的参数 需要工具 loaderUtils
let loaderUtils = require('loader-utils')
function loader(source) { // loader的参数就是源代码
// file-loader需要返回路径
let filename = loaderUtils.interpolateName(this, '[hash].[ext]', {content: source })
this.emitFile(filename, source) // 发射文件
console.log('loader1');
return module.exports="${filename}"
}
loader.raw = true // 二进制
module.exports = loader
创建url-loader1.js
// 拿到babel的参数 需要工具 loaderUtils
let loaderUtils = require('loader-utils')
let mime = require('mime') // 途是设置某种扩展名的文件的响应程序类型
function loader(source) { // loader的参数就是源代码
let {limit} = loaderUtils.getOptions(this)
console.log(this.resourcePath);
if (limit && limit > source.length) {
return module.exports="data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"
} else {
return require('./file-loader1').call(this, source)
}
}
loader.raw = true // 二进制
module.exports = loader
webpack.config.js
{
test: /.png$/,
// 目的是根据图片生成md5 发射到dist目录下,file-loader 返回当前图片路径
// use: 'file-loader'
// 处理路径
use: {
loader: 'url-loader1',
options: {
limit: 200 * 1024
}
}
}
index.js引入图片
import p from './photo.png'
let img = document.createElement('img')
img.src = p
document.body.appendChild(img);
less-loader和css-loader
先安装less
分别创建style-loader2 css-loader2 less-loader2
style-loader1 与 less-loader1 同之前的
css-loader
主要用来处理css中的图片链接,需要把url转换成require
webpack.config.js
{
test: /.png$/,
// 目的是根据图片生成md5 发射到dist目录下,file-loader 返回当前图片路径
// use: 'file-loader'
// 处理路径
use: {
loader: 'url-loader1',
options: {
limit: 200 * 1024
}
}
},
{
test: /.less$/,
use: ['style-loader2', 'css-loader2', 'less-loader2']
}
创建index.less
@base: #f938ab;
body {
background: @base;
background: url("./photo.png");
}
less-loader2.js
// 将less转为css
let less = require('less')
function loader(source) {
let css = ''
// console.log(source, 2222);
less.render(source, function (err, output) {
// console.log(output);
css = output.css
})
// css = css.replace(/
/g, '
');
return css
}
module.exports = loader
css-loader2.js
// css-loader 用来解析@import这种语法,包括css中引入的图片
function loader(source) {
let reg = /url((.+?))/g // 匹配括号
let pos = 0;
let current;
let arr = ['let list = []']
while (current = reg.exec(source)) {
let [matchUrl, g] = current // matchUrl -> 'url("./photo.png")', g -> '"./photo.png"'
// console.log(matchUrl, g, 88);
let lastIndex = reg.lastIndex - matchUrl.length // 拿到css从开通到地址链接之前的index
arr.push(`list.push(${JSON.stringify(source.slice(pos, lastIndex))})`) // 拼入开始和地址之前的代码
pos = reg.lastIndex
arr.push(`list.push('url('+ require(${g}) +')')`) // 拼入图片地址
}
arr.push(`list.push(${JSON.stringify(source.slice(pos))})`) // 拼入地址到结尾的代码
arr.push(`module.exports = list.join('')`)
console.log(arr.join('
'));
// let list = []
// list.push("body {\n background: #f938ab;\n background: ")
// list.push('url('+ require("./photo.png") +')')
// list.push(";\n}\n")
// module.exports = list.join('')
return arr.join('
')
}
module.exports = loader
style-loader2.js
let loaderUtils = require('loader-utils')
// 将css插入到html头部
function loader(source) {
let str = let style = document.createElement('style') style.innerHTML = ${JSON.stringify(source)} document.head.appendChild(style)
return str
}
// style-loader写了pitch,有返回后面的跳过,自己的写不会走
loader.pitch = function (remainingRequest) { // 剩余的请求
console.log(loaderUtils.stringifyRequest(this, '!!' + remainingRequest, 99999999))
// 让style-loader 处理 less-loader 和css-loader拼接的结果
// 得到 /Users/liuhuimin/work/webpack/loader/css-loader2.js!/Users/liuhuimin/work/webpack/loader/less-loader2.js!/Users/liuhuimin/work/webpack/src/index.less
// 剩余的请求 less-loader!css-loader!./index.less
// console.log(remainingRequest, 1223);
// require返回的就是css-loader处理好的结果require('!!css-loader!less-loader!./index.less')
let str = let style = document.createElement('style') style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}) document.head.appendChild(style)
// stringifyRequest 绝对路径转相对路径
return str
}
module.exports = loader
user: ['style-loader2', 'css-loader2', 'less-loader2']
pitch loader - 有返回值
pitch style-loader2 → css-loader2 less-loader2
↙
有返回值 资源
↙
normal style-loader2 css-loader2 less-loader2
在style-loader2中 引用了less-loader css-loader 和less文件
webpack 中的插件
yarn add webpack webpack-cil -D
webpack.config.js
let path = require('path')
let DonePlugin = require('./plugins/DonePlugins')
let AsyncPlugins = require('./plugins/AsyncPlugins')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'build.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new DonePlugin(), // 同步
new AsyncPlugins() // 异步
]
}
node_modules/webpack/lib中查看Compiler.js
同步plugins/DonePlugins
打包完成
class DonePlugins {
apply (compiler) {
console.log(1);
compiler.hooks.done.tap('DonePlugin', (stats) => {
console.log('编译完成');
})
}
}
module.exports = DonePlugins
异步plugins/AsyncPlugins
class AsyncPlugins {
apply (compiler) {
console.log(2);
compiler.hooks.emit.tapAsync('AsyncPlugin', (complete, callback) => {
setTimeout(() => {
console.log('文件发射出来');
callback()
}, 1000)
})
compiler.hooks.emit.tapPromise('AsyncPlugin', (complete, callback) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('文件发射出来 222');
resolve()
}, 1000)
})
})
}
}
module.exports = AsyncPlugins
文件列表插件
希望生成一个文件描述打包出来的文件
在plugins中新建FileListPlugin
class FileListPlugin {
constructor ({filename}) {
this.filename = filename
}
apply (compiler) {
// 文件已经准备好了 要进行发射
// emit
compiler.hooks.emit.tap('FileListPlugin', (compilation) => {
let assets = compilation.assets;
console.log(assets, 55);
let content = ## 文件名 资源大小
// [ [bundls.js, {}], [index.html, {}]]
Object.entries(assets).forEach(([filename, stateObj]) => {
content += - ${filename} ${stateObj.size()}
})
// 资源对象
assets[this.filename] = {
source () {
return content;
},
size () {
return content.length
}
}
})
}
}
module.exports = FileListPlugin
let path = require('path')
let DonePlugin = require('./plugins/DonePlugins')
let AsyncPlugins = require('./plugins/AsyncPlugins')
let HtmlWebpackPlugin = require('html-webpack-plugin')
let FileListPlugin = require('./plugins/FileListPlugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'build.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new DonePlugin(),
new AsyncPlugins(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new FileListPlugin({
filename: 'list.md'
})
]
}
生成list.md
内联的webpack插件
新建index.css引入index.js
yarn add css-loader mini-css-extract-plugin -D
希望打包后css、js内联在index.html文件中
创建plugins中InlineSourcePlugins.js
yarn add --dev html-webpack-plugin@next
HTML Webpack Plugin
webpack.config.js
let path = require('path')
let DonePlugin = require('./plugins/DonePlugins')
let AsyncPlugins = require('./plugins/AsyncPlugins')
let HtmlWebpackPlugin = require('html-webpack-plugin')
let FileListPlugin = require('./plugins/FileListPlugin')
let InlineSourcePlugins = require('./plugins/InlineSourcePlugins')
let MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
// new DonePlugin(),
// new AsyncPlugins(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'index.css'
}),
new InlineSourcePlugins({
match: /.(js|css)/
}),
// new FileListPlugin({
// filename: 'list.md'
// })
]
}
InlineSourcePlugins.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 把外链的标签编程内联的标签
class InlineSourcePlugins {
constructor({match}) {
this.reg = match // 正则
}
// 处理某一个标签
processTag(tag, compilation) {
let newTag = {}
let url = ''
if (tag.tagName === 'link' && this.reg.test(tag.attributes.href)) {
newTag = {
tagName: 'style',
attributes: {type: 'text/css'}
}
url = tag.attributes.href
} else if (tag.tagName === 'script' && this.reg.test(tag.attributes.src)) {
newTag = {
tagName: 'script',
attributes: {type: 'application/javascript'}
}
url = tag.attributes.src
}
if (url) {
newTag.innerHTML = compilation.assets[url].source(); // 文件内容放到innerHTML属性中
delete compilation.assets[url] // 删除原有的资源
return newTag
// console.log(compilation.assets[url].source());
}
return tag
}
// 处理引入标签的数据
processTags(data, compilation) {
let headTags = []
let bodyTags = []
data.headTags.forEach(headTag => {
headTags.push(this.processTag(headTag, compilation))
})
data.bodyTags.forEach(bodyTag => {
bodyTags.push(this.processTag(bodyTag, compilation))
})
console.log({...data, headTags, bodyTags})
return {...data, headTags, bodyTags}
}
apply(compiler) {
// 通过webpackPlugin来实现 npm搜索 html-webpack-plugin
compiler.hooks.compilation.tap('InlineSourcePlugins', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync(
'alertPlugin',
(data, callback) => {
// console.log('======');
// console.log(data) // 插入html标签的数据
// console.log('======');
data = this.processTags(data, compilation) // compilation.assets 资源的链接
callback(null, data)
})
})
}
}
module.exports = InlineSourcePlugins
打包后自动发布
打包好的文件自动上传致七牛
需要这几个参数
bucket: '' // 七牛的存储空间
domain: '',
accessKey: '', // 七牛云的两对密匙
secretKey: '' // 七牛云的两对密匙
注册七牛,并在对象存储里面,新建存储空间列表test,bucket: 'test'
内容管理外链接默认域名 domain: 'xxxxxxxx'
右上角个人面板里面个人中心,密钥管理分别对应accessKey和secretKey
进入开发者中心 -> SDK&工具 -> 官方SDK -> Node服务端文档 —> 文件上传
node文件上传
npm install qiniu
compiler-hooks
webpack.config.js
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'index.css'
}),
new UploadPlugin({
bucket: 'test', // 七牛的存储空间
domain: 'poyrjyh1b.bkt.clouddn.com',
accessKey: 'xxxxxx', // 七牛云的两对密匙
secretKey: 'yyyyyy' // 七牛云的两对密匙
})
]
UploadPlugin.js
let qiniu = require('qiniu')
let path = require('path')
class UploadPlugin {
constructor (options = {}) {
// 参考 https://developer.qiniu.com/kodo/sdk/1289/nodejs
let { bucket = '', domain = '', accessKey = '', secretKey = ''} = options
let mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
let putPolicy = new qiniu.rs.PutPolicy({
scope: bucket
});
this.uploadToken = putPolicy.uploadToken(mac)
let config = new qiniu.conf.Config();
this.formUploader = new qiniu.form_up.FormUploader(config)
this.putExtra = new qiniu.form_up.PutExtra()
}
apply (compiler) {
compiler.hooks.afterEmit.tapPromise('UploadPlugin', (complication) => {
let assets = complication.assets
let promise = []
Object.keys(assets).forEach(filename => {
promise.push(this.upload(filename))
})
return Promise.all(promise)
})
}
upload (filename) {
return new Promise((resolve, reject) => {
let localFile = path.resolve(__dirname, '../dist', filename)
this.formUploader.putFile(this.uploadToken, filename, localFile, this.putExtra, function(respErr,
respBody, respInfo) {
if (respErr) {
reject(respErr)
}
if (respInfo.statusCode == 200) {
resolve(respBody)
} else {
console.log(respInfo.statusCode)
console.log(respBody)
}
});
})
}
}
module.exports = UploadPlugin