webpack中的核心概念
entry
用于指定本次webpack打包的地址(相对地址即可)如:
单入口
entry:'./src/index.js'
或:
entry:{
main:'./src/index.js'
}
多入口
entry:{
main:'./src/index.js',
other:'./src/other.js'
}
output
用于指定打包完成之后的输出文件地址及文件名,文件地址使用绝对地址
单文件
output:{
filename:'bundle.js',
path:path.join(__dirname,'dist')
}
多文件
output:{
filename:'[name].js',
path:path.join(__dirname,'dist')
}
mode
用于指定当前构建环境
主要有以下三种选择
production
development
none
设置mode会自动触发webpack的一些内置函数,不写默认为none
loaders
webpack默认是能识别.json、.js、模块,其他模块我们需要借助loaders帮助我们将它们放进依赖图里面
它本质就是一个函数,接收源文件为参数,返回转换后的结果
plugins
plugin可以在webpack运行到某个阶段的时候(webpack利用 tapable搞了许多生命周期的构造,方便我们在合适的时间利用插件帮我们做些合适的事情
做一些我们需要的事情。如clean-webpack-plugin会在我们进打包的时候先删除dist下的原输出文件。
一:基础使用
1.1 处理html、css、js
使用webpack-dev-server
我们也是希望自己打包后文件可以在一个本地服务器上启动,webpack-dev-server就是一个这种东西。
安装:npm i webpack-dev-server -D
「新配置webpack.config.js一个devServer属性」
如下:
devServer: {
port: 3000,
contentBase: './dist'
open: true
compress:true
}
「为简化命令在package.json中添加一个dev命令」
同时也可以在配一个打包命令
{
"name": "webpack-test02",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build":"webpack",
"dev": "webpack-dev-server",
},
}
使用html-webpack-plugin
可能上面你发现了,没有一个html文件。即使服务器启动也没有什么卵用,
接下来我们可以在文件中写一个html模板(只生成骨架即可)然后利用html-webpack-plugin将这个模板同时打包到dist目录下,并引入输出的bundl.js
安装:npm i html-webpack-plugin -D
使用:
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
hash: true,
}),
],
还有以下选项
minify: {
removeAttributeQuotes: true,
collapseWhitespace: true
}
处理css
基本处理
这里需要安装两个loader,即css-loader(用于处理css中的@import这种语法)、style-loader用于将css插入head标签
安装:npm i css-loader style-loader -D
使用:
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
]
}
或指定参数,下面是每次将css插入到head标签的前面。以保证我们后面自己在模板中的设置可覆盖
module: {
rules: [{
test: /\.css$/,
use: [{
loader: 'style-loader',
options: {
insert: function insertAtTop(element) {
var parent = document.querySelector('head');
var lastInsertedElement =
window._lastElementInsertedByStyleLoader;
if (!lastInsertedElement) {
parent.insertBefore(element, parent.firstChild);
} else if (lastInsertedElement.nextSibling) {
parent.insertBefore(element, lastInsertedElement.nextSibling);
} else {
parent.appendChild(element);
}
window._lastElementInsertedByStyleLoader = element;
},
}
},
'css-loader'
]
},
]}
抽离css
使用插件 mini-css-extract-plugin
安装:npm i mini-css-extract-plugin -D
抽离成了单文件即不需要在用style-loader了
使用:
const MinniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new MinniCssExtractPlugin({
filename: 'main.css'
})
],
module: {
rules: [{
test: /\.css$/,
use: [
MinniCssExtractPlugin.loader,
'css-loader',
]
},
]
}
压缩css、js
将上面输出的css文件做压缩处理
使用插件optimize-css-assets-webpack-plugin但是使用了此插件压缩css之后,js需要使用uglifyjs-webpack-plugin继续压缩
安装:npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
使用:(注意此插件不再在plugin中使用了,使用位置为优化optimization属性里面)
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true
}),
new OptimizeCSSAssetsPlugin({})
]
},
添加厂商前缀
需要一个loader和一个样式工具:postcss-loader autoprefixer
「先来说一下什么是postCss」
它是一个编译插件的容器,它的工作模式为接收源代码交由编译插件处理,最后输出css。
postcss-loader就是postCss和webpack的连接器。
postcss-loader可以和css-loader一起使用也可以单独使用。
注意单独使用postcss-loader的使用css中不建议使用@import语法,否则会产生冗余代码。
postCss还还需要一个单独的配置文件postcss.config.js
下面还看用post-loader和autoprefixer 来生成厂商前缀
安装:npm i postcss-loader autoprefixer -D
使用:webpack.config.js,module的rules中
{
test: /\.css$/,
use: [
MinniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
]
},
postcss.config.js中
module.exports = {
plugins: [
require("autoprefixer")({
overrideBrowserslist: ["last 2 versions", ">1%"]
})
]
};
处理less
需要使用 less-loader,同时less-loader里面利用的是less故需安装less less-loader
安装:npm i less-loader less -D
使用:
{
test: /\.less$/,
use: [
MinniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
}
sass同理
处理js
es6转es5
安装:npm i babel-loader @babel/core @babel/preset-env -D
使用:
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
],
}
}
},
es7 转class语法
安装:npm i @babel/plugin-proposal-class-properties -D
使用:
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
],
plugins:[
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
}
},
其他详细见babel官网
1.2 处理图片
一张图片的引用有三种方式
js中引入 创建图片标签引入
css引入 url
html中引入 img
使用file-loader
第三张情况不可用,需要额外使用loader
安装:npm i file-loader -D
本质就是引用是返回的是一张图片的地址,不过此张图片是已经在bundle中了
使用:「js中」
import './index.css'
import img from '../public/img/img01.jpg'
const image = new Image()
image.src = img
document.body.appendChild(image)
「css中」
div{
p{
width: 100px;
height: 100px;
transform: rotate(45deg);
background-image: url('./img01.jpg');
}
}
「webpack.config.js中」
module.export={
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader'
}
]
}
}
使用 url-loader
可当做file-loader的升级版,我们可以设置一张图片小于多少时使用base64,或者使用file-loader打包原图片
安装:npm i url-loader -D
同时这里可以使用html-withimg-loader处理html中的图片了,注意要把esModule属性设置为false。否则链如html中图片地址不对上面同理
安装:npm i html-withimg-loader -D
使用:
{
test: /\.html$/,
use: 'html-withimg-loader'
}, {
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
limit: 50 * 1024,
loader: 'file-loader',
esModule: false,
outputPath: '/img/',
}
}
},
1.3 eslint
1.4 常用小插件
cleanWebpackPlugin
每次打包自动删除输出目录下的文件
安装:npm i clean-webpack-plugin -D
使用:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins:[
new CleanWebpackPlugin(),
]
copyWebpackPlugin
可以一些没有依赖到,但是又需要输出倒dist下的文件
安装:npm i copy-webpack-plugin -D
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, 'public'),
to: 'dist'
}],
}),
]
bannerPlugin
代码首部添加版权
webpack上的一个自带插件
使用:
const webpack = require('webpack')
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, 'public'),
to: 'dist'
}],
}),
new webpack.BannerPlugin('core by gxb'),
]
二:进阶用法
2.1 多页面打包
即多入口,每个入口对应一个生成依赖树
配置很简单,配一下入口。入口格式为一个chunkNmae:path
entry: {
index: './src/index.js',
other: './src/other.js'
},
output: {
filename: '[name].js',
path: path.join(__dirname, './dist')
},
2.2 devtool :‘source-map’
即源代码与打包后的代码之间的映射,就是在代码发生问题时可以通过source-map定位到原代码块
主要是在开发环境下使用
配置如下
devtool: 'source-map'
devtool属性值有许多种,下面总结几个最常用的
source-map 会产生映射.map文件,同时也能定位到行列
cheap-module-source-map,不会产生.map文件,可定位到行【推荐配置】
eval-source-map,不会产生.map文件,可定位行列
注意对于css、less、scss来说要想能定位到源码,还需在loader选项中进行配置
如:(请注意:笔者偷懒此段代码没有进行测试不知道现在过时没有)
test:\/.css$\,
use:[
'style-loader',
{
loader:'css-loader',
options:{
sourceMap:true
}
}
]
2.3 watch
webpack中也可以配置watch监听器进行时时打包,即我们每次修改完代码之后不需要在自己输入命令了,直接c+s键保存即可。
配置如下:
module.exports = {
watch: true,
watchOptions: {
poll: 1000,
aggregateTimeout: 500,
ignored: /node_modules/
}
}
2.4 resolve
我们都清楚webpack启动之后会从入口文件开始寻找所有的依赖,
但是像寻找一些第三方包的时候它总是会默认去找这个文件main.js为入口文件,
但是像bootstrap我们有时候仅仅是需要引用它的样式。如果将它全部拿来打包,那么是不是有点太浪费了呢
resole在这里便可以指定webpack如何寻找模块对象的文件
配置如下:
resolve:{
modules:[path.join('node_modules')],
mainFields: ['style', 'main'],
extensions: ['.js', '.css', '.json']
alias:{
components: './src/components/'
}
}
2.5 环境拆分
开发和线上的环境所需要配置的东西一般是不相同的,故可以利用webpack-merge,将配置文件拆分成一个基础公共、一个开发、一个线上的。
我们以后打包时就可以指定指定配置文件用于开发环境下打包或者产品环境下打包了。
安装:npm i webpack-merge -D
写法如下:
基础
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new CleanWebpackPlugin(),
],
output: {
filename: 'bundle.js',
path: path.join(__dirname, './dist')
},
}
开发:webpack.dev.js
const merge = require('webpack-merge')
const common = require('./webpack.config')
module.exports = merge(base, {
mode: 'development',
devServer: {},
devtool: 'source-map'
})
线上:webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');
module.exports = merge(common, {
mode: 'production',
});
2.6 处理跨域
跨域,即利用代理处理跨域的思想就是:同源策略仅是存在于浏览器,服务器之间是不存在的。
故我们可以先把数据发到一个代理服务器上
例子:像一个不同域服务器发送请求
const xhr = new XMLHttpRequest();
xhr.open('get', '/api/user', true);
xhr.send();
xhr.onload = function () {
console.log(xhr.response)
}
服务器代码:
const express = require('express')
const app = express()
app.get('/test', (req, res) => {
res.json({ msg: 11 })
})
app.listen(3001, function() {
console.log('启动服务');
})
webpack.config.js配置代理
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:3001',
pathRewrite: { '^/api': '' }
}
},
progress: true,
contentBase: './dist',
open: true
},
三:优化
3.1 noparse
引用一些第三方包的时候,我们是不需要再进入这些包中再去寻找依赖,因为一般情况下均是独立的。
配置如下:
module: {
noParse: /jquery/,
rules:[]
}
3.2 include&exclude
同时我们也可以指定寻找的范围
如:
rules: [
{
test: /\.js$/,
exclude: '/node_modules/',
include: path.resolve('src'),
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
]
}
}
}
不建议使用exclude,要使用绝对路径
3.3 lgnorePlugin插件
有些第三方库中许多东西是我们用不到的,如一些有语言包的库。
我们一般情况下是仅需要使用它里面的中文包的,其他各种语言包我们是不需要的。
故这是可以使webpack中提供的一个插件
const webpack = require('webpack')
plugins: [
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
3.4 多线程打包
使用插件happypack
安装:npm i happypack
使用:
const Happypack = require('happypack')
module:{
rules: [{
test: /\.css$/,
use: 'happypack/loader?id=css'
}]
}
plugins:[
new Happypack({
id: 'css',
use: ['style-loader', 'css-loader']
})
]
3.5 懒加载(按需加载)
即有些东西我们不用马上把它导入我们的依赖树中,而是用到它之后再导进来(就类似于按需加载)
这里需要@babel/plugin-syntax-dynamic-import用于 解析识别import()动态导入语法
安装:npm i @babel/plugin-syntax-dynamic-import -D
index.js中:
const button = document.createElement('button')
button.innerHTML = '按钮'
button.addEventListener('click', () => {
console.log('click')
import ('./source.js').then(data => {
console.log(data.default)
})
})
document.body.appendChild(button)
source.js
export default 'gxb'
webpack.config.js
{
test: /\.js$/,
include: path.resolve('src'),
use: [{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
],
plugins: [
'@babel/plugin-syntax-dynamic-import'
]
}
}]
}
即source.js模块仅仅当我们点击了按钮之后才会加入依赖,否则不会立即打包
3.6 热更新
即代码更新是页面只会更新该更新的地方而不是重新渲染整个页面,即重新刷新页面
热更新插件也是webpack上自带的
配置如下:
devServer: {
hot: true,
port: 3000,
contentBase: './dist',
open: true
},
plugins:[
new webpack.HotModuleReplacementPlugin()
]
3.7 抽取公共代码
尤其是在一些多入口文件中,如入口index.js引用了a.js,又一个入口的other.js又引用了a.js,那么正常情况下a.js会被打印两次。
抽取:
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
common: {
minSize: 0,
minChunks: 2,
chunks: 'initial'
}
}
}
},
}
抽取一些像jquery这样的一般被多次引用的第三方包
optimization: {
splitChunks: {
cacheGroups: {
common: {
minSize: 0,
minChunks: 2,
chunks: 'initial'
},
vendor: {
priority: 1,
test: /node_modules/,
minSize: 0,
minChunks: 2,
chunks: 'initial'
}
}
},
},
四:tapable——手写早知道
tapable是一个类似于nodejs的eventEmitter的库,主要功能是控制各种钩子函数的发布与订阅,控制着webpack的插件系统
4.1 同步
4.1.1 SyncHook的用法与实现
最没有特点的一个hook,就是同步串行
const { SyncHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new SyncHook(['name']),
}
}
start() {
this.hooks.arch.call('gxb')
}
tap() {
this.hooks.arch.tap('node', function(name) {
console.log('node', name)
})
this.hooks.arch.tap('react', function(name) {
console.log('react', name)
})
}
}
const l = new Lesson()
l.tap();
l.start()
class SyncHook {
constructor() {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
this.tasks.forEach(item => {
item.cb(...arg)
})
}
}
const syncHook = new SyncHook()
syncHook.tap('node', name => {
console.log('node', name)
})
syncHook.tap('vue', name => {
console.log('vue', name)
})
syncHook.call('gxb')
4.1.2 SyncBailHook的用法与实现
const { SyncBailHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new SyncBailHook(['name'])
}
}
tap() {
this.hooks.arch.tap('node', (name) => {
console.log('node', name);
return 'error'
})
this.hooks.arch.tap('vue', (name) => {
console.log('vue', name);
return undefined
})
}
start() {
this.hooks.arch.call('gxb')
}
}
const l = new Lesson()
l.tap()
l.start()
class SyncBailHook {
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
let ret
let index = 0
do {
ret = this.tasks[index++].cb(...arg)
} while (ret === undefined && index < this.tasks.length);
}
}
const syncBailHook = new SyncBailHook()
syncBailHook.tap('node', name => {
console.log('node', name);
return 'error'
})
syncBailHook.tap('vue', name => {
console.log('vue', name);
})
syncBailHook.call('gxb')
4.1.3 SyncWaterfallHook的用法与与实现
const { SyncWaterfallHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new SyncWaterfallHook(['name'])
}
}
tap() {
this.hooks.arch.tap('node', (name) => {
console.log('node', name);
return 'node ok'
})
this.hooks.arch.tap('vue', (data) => {
console.log('vue', data);
})
}
start() {
this.hooks.arch.call('gxb')
}
}
const l = new Lesson()
l.tap()
l.start()
class SyncWaterfallHook {
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
let [first, ...others] = this.tasks
let ret = first.cb(...arg)
others.reduce((pre, next) => {
return next.cb(pre)
}, ret)
}
}
const syncWaterfallHook = new SyncWaterfallHook()
syncWaterfallHook.tap('node', data => {
console.log('node', data);
return 'error'
})
syncWaterfallHook.tap('vue', data => {
console.log('vue', data);
})
syncBailHook.call('gxb')
4.1.4 SyncLoopHook的用法与实现
const { SyncLoopHook } = require('tapable')
class Lesson {
constructor() {
this.index = 0
this.hooks = {
arch: new SyncLoopHook(['name'])
}
}
tap() {
this.hooks.arch.tap('node', (name) => {
console.log('node', name);
return ++this.index === 3 ? undefined : this.index
})
this.hooks.arch.tap('vue', (name) => {
console.log('vue', name);
return undefined
})
}
start() {
this.hooks.arch.call('gxb')
}
}
const l = new Lesson()
l.tap()
l.start()
class SyncLoopHook {
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
this.tasks.forEach(item => {
let ret;
do {
ret = item.cb(...arg)
} while (ret !== undefined);
})
}
}
const syncLoopHook = new SyncLoopHook()
let index = 0
syncLoopHook.tap('node', name => {
console.log('node', name);
return ++index === 3 ? undefined : index
})
syncLoopHook.tap('vue', name => {
console.log('vue', name);
})
syncLoopHook.call('gxb')
4.2 异步
异步并发
4.2.1 AsyncParallelHook
const { AsyncParallelHook } = require("tapable")
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(['name'])
}
}
tap() {
this.hooks.arch.tapAsync('node', (name, cb) => {
setTimeout(() => {
console.log("node", name);
cb();
}, 1000);
})
this.hooks.arch.tapAsync('vue', (name, cb) => {
setTimeout(() => {
console.log('vue', name)
cb()
}, 1000)
})
}
start() {
this.hooks.arch.callAsync('gxb', function() {
console.log('end')
})
}
}
let l = new Lesson();
l.tap();
l.start();
class SyncParralleHook {
constructor() {
this.tasks = []
}
tapAsync(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
callAsync(...arg) {
const lastCb = arg.pop()
let index = 0
const done = () => {
index++
if (index === this.tasks.length) {
lastCb()
}
}
this.tasks.forEach(item => item.cb(...arg, done))
}
}
const hook = new SyncParralleHook()
hook.tapAsync('node', (name, cb) => {
setTimeout(function() {
console.log('node', name)
cb()
}, 1000)
})
hook.tapAsync('vue', (name, cb) => {
setTimeout(function() {
console.log('vue', name)
cb()
}, 1000)
})
hook.callAsync('gxb', function() {
console.log('end')
})
const { AsyncParallelHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(['name'])
}
}
start() {
this.hooks.arch.promise('gxb').then(function() {
console.log('end')
})
}
tap() {
this.hooks.arch.tapPromise('node', name => {
return new Promise((resove, reject) => {
console.log('node', name)
resove()
})
})
this.hooks.arch.tapPromise('vue', name => {
return new Promise((resove, reject) => {
console.log('vue', name)
resove()
})
})
}
}
const l = new Lesson()
l.tap()
l.start()
class AsyncParallelHook {
constructor() {
this.tasks = []
}
tapPromise(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
promise(...arg) {
const newTasks = this.tasks.map(item => item.cb(...arg))
return Promise.all(newTasks)
}
}
const hook = new AsyncParallelHook()
hook.tapPromise('node', name => {
return new Promise((res, rej) => {
console.log('node', name)
res()
})
})
hook.tapPromise('vue', name => {
return new Promise((res, rej) => {
console.log('vue', name)
res()
})
})
hook.promise('gxb').then(function() {
console.log('end')
})
4.2.2AsyncParallelBailHook
和同步同理
异步串行
4.2.3 AsyncSeriesHook
const { AsyncSeriesHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncSeriesHook(['name'])
}
}
start() {
this.hooks.arch.callAsync('gxb', function() {
console.log('end')
})
}
tap() {
this.hooks.arch.tapAsync('node', (name, cb) => {
setTimeout(() => {
console.log('node', name)
cb()
}, 1000)
})
this.hooks.arch.tapAsync('vue', (name, cb) => {
setTimeout(() => {
console.log('node', name)
cb()
}, 1000)
})
}
}
const l = new Lesson()
l.tap()
l.start()
class AsyncSeriesHook {
constructor() {
this.tasks = []
}
tapAsync(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
callAsync(...arg) {
const finalCb = arg.pop()
let index = 0
let next = () => {
if (this.tasks.length === index) return finalCb()
let task = this.tasks[index++].cb
task(...arg, next)
}
next()
}
}
const hook = new AsyncSeriesHook()
hook.tapAsync('node', (name, cb) => {
setTimeout(() => {
console.log('node', name)
cb()
}, 1000)
})
hook.tapAsync('vue', (name, cb) => {
setTimeout(() => {
console.log('vue', name)
cb()
}, 1000)
})
hook.callAsync('gxb', function() {
console.log('end')
})
4.2.4 AsyncSeriesBailHook
参考同步
4.2.5 AsyncSeriesWaterfallHook
参考同步
五:手写一个简单的webpack
5.1 构建入口文件,即link到本地
初始化一个新的项目,首先npm init -y生成package.json
写一个bin命令
"bin": {
"mypack": "./bin/index.js"
},
./bin/index.js作为我们的入口文件
再运行npm link, 将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试
再创建一个写了webpack.config.js文件的项目(先称之为要打包的项目文件吧——>源码项目)。运行命令`npm link mypack
这是在要打包的项目中运行命令 npx mypack,其所用的就是我们手写的那个了
5.2 构建核心,compiler类的编写
回到手写webpack项目下初始化compiler类
入口文件中没什么东西
只是需要拿到源码项目下的webpack.config.js,接下来调用compiler方法将拿到的配置文件地址传进去
#! /usr/bin/env node
const path = require('path')
const config = require(path.resolve('webpack.config.js'))
const Compiler = require('../lib/compiler.js')
const compiler = new Compiler(config)
compiler.run()
Compiler类的基本骨架
const path = require('path')
const fs = require('fs')
const tapable = require('tapable')
class Compiler {
constructor(config) {
this.config = config
this.entryId = ''
this.modules = {}
this.entry = config.entry
this.root = process.cwd()
this.asserts = {}
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncHook(),
}
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
item.run(this)
})
}
}
buildMoudle(modulePath, isEntry) {}
emitFile() {}
run() {
this.hooks.entryInit.call()
this.buildMoudle(path.resolve(this.root, this.entry), true)
this.hooks.afterCompile.call()
this.emitFile()
}
}
构建模板
首先run中传过来的是一个入口地址,和一个标志是不是入口的布尔。我们要做的是首先入口的相对地址赋值给this.entryId
然后获取文件源码,搞成{文件相对地址:改造后源码}这种键值对的形式
buildMoudle(modulePath, isEntry) {
let source = this.getSource(modulePath)
let moduleName = './' + path.relative(this.root, modulePath)
if (isEntry) {
this.entryId = moduleName
}
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
this.modules[moduleName] = sourceCode
dependencies.forEach(item => {
this.buildMoudle(path.resolve(this.root, item), false)
})
}
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
改造源码
这里要用到一些工具
babylon 将源码搞成AST抽象语法树
@babel/traverse用于替换AST上的节点
@babel/generator 结果生成
@babel/types AST节点的Lodash-esque实用程序库
安装:npm i babylon @babel/traverse @babel/types @babel/generator
源码:就是将require换成__webpack_require__,同时在改造一下require的内部路径参数。其实webpack可是打包require本质就是手动实现了一个require方法
注意:本次只是重写了require,可仍是不支持es6模块的啊
parse(source, parentPath) {
let ast = babylon.parse(source)
let dependencies = []
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')
moduledName = './' + path.join(parentPath, moduledName)
dependencies.push(moduledName)
node.arguments = [type.stringLiteral(moduledName)]
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
输出到output的指定位置
emitFile() {
let outPath = 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.asserts[outPath] = code
console.log(code);
fs.writeFileSync(outPath, this.asserts[outPath])
}
(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;
}
return __webpack_require__(__webpack_require__.s = "<%-entryId %>");
})({
<% for(let key in modules){ %>
"<%- key %>":
(function (module, exports,__webpack_require__) {
eval(`<%-modules[key] %>`);
}),
<% } %>
});
加入loader
写一个less-loader和style-loader,css-loader只是处理一些css中的@import的语法,故这里为求简便不写css-loader了
less-loader:注要就是借用less,将less文件转为css文件
const less = require('less')
function loader(source) {
let css = ''
less.render(source, function(err, output) {
css = output.css
})
css = css.replace(/\n/g, '\\n')
return css
}
module.exports = loader
style-loader:搞一个标签,将css放进去
function loader(source) {
let style = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)}
document.head.appendChild(style)
`
return style
}
module.exports = loader
此时的webpack.config.js
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.join(__dirname, './dist')
},
module: {
rules: [{
test: /\.less$/,
use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]
}]
},
plugins: [
new TestPlugins(),
new InitPlugin()
]
}
此时源码文件获取函数改造如下
getSource(modulePath) {
const rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
if (test.test(modulePath)) {
console.log(111);
function normalLoader() {
let loader = require(use[--len])
content = loader(content)
if (len > 0) {
normalLoader()
}
}
normalLoader()
}
}
return content
}
并添加插件
直接在源码项目的webpack.config.js中编写吧
const path = require('path')
class TestPlugins {
run(compiler) {
compiler.hooks.afterCompile.tap('TestPlugins', function() {
console.log(`this is TestPlugins,runtime ->afterCompile `);
})
}
}
class InitPlugin {
run(compiler) {
compiler.hooks.entryInit.tap('Init', function() {
console.log(`this is InitPlugin,runtime ->entryInit `);
})
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.join(__dirname, './dist')
},
module: {
rules: [{
test: /\.less$/,
use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]
}]
},
plugins: [
new TestPlugins(),
new InitPlugin()
]
}
compiler类全部代码
const path = require('path')
const fs = require('fs')
const { assert } = require('console')
const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
const ejs = require('ejs')
const tapable = require('tapable')
class Compiler {
constructor(config) {
this.config = config
this.entryId = ''
this.modules = {}
this.entry = config.entry
this.root = process.cwd()
this.asserts = {}
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncHook(),
}
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
item.run(this)
})
}
}
getSource(modulePath) {
const rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
if (test.test(modulePath)) {
console.log(111);
function normalLoader() {
let loader = require(use[--len])
content = loader(content)
if (len > 0) {
normalLoader()
}
}
normalLoader()
}
}
return content
}
parse(source, parentPath) {
let ast = babylon.parse(source)
let dependencies = []
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')
moduledName = './' + path.join(parentPath, moduledName)
dependencies.push(moduledName)
node.arguments = [type.stringLiteral(moduledName)]
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
buildMoudle(modulePath, isEntry) {
let source = this.getSource(modulePath)
let moduleName = './' + path.relative(this.root, modulePath)
if (isEntry) {
this.entryId = moduleName
}
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
this.modules[moduleName] = sourceCode
dependencies.forEach(item => {
this.buildMoudle(path.resolve(this.root, item), false)
})
}
emitFile() {
let outPath = 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.asserts[outPath] = code
console.log(code);
fs.writeFileSync(outPath, this.asserts[outPath])
}
run() {
this.hooks.entryInit.call()
this.buildMoudle(path.resolve(this.root, this.entry), true)
this.hooks.afterCompile.call()
this.emitFile()
}
}
module.exports = Compiler