webpck-dev-ser作为开发时的服务非常方便,本文将对webpack-dev-server从原理层次解析,来看看它是如何实现服务和热更新的。
指令说明:
-- webpack-dev-server 实现自动打包编译功能(每次修改JS文件后,都需要webpack执行打包重新生成JS文件。
1、它会把整个项目以localhost服务形式运行起来,虚拟了一个服务器;
2、webpack-dev-server会把webpack打包输出文件会被托管于(URL)根路径(本地磁盘dist目录下的不会发生改变),可以直接服务器根路径+输出JS文件名访问到)
-- open,编译完自动打开浏览器
-- port 端口,更改运行端口(默认8080)
-- contentBase 路径,更改内容根路径(默认服务器根路径、项目根路径),也是托管路径,可以设置为src即刚打开浏览器就访问到页面。引用路径时需要注意这个(例如项目根路径有node-modules文件夹,默认可以访问到;修改为src,即根路径变为src,手动引用时会访问不到)安装了html-webpack-plugin后,页面也托管于根路径可以直接访问到,此参数可不需要。
-- hot,热重载、热跟新,页面异步刷新,减少不必要的刷新请求;打补丁,而不是重新编译,减少不必要的代码跟新。
1,webpack-dev-server的服务原理
基于express,搭建了一个http服务,根据路由返回不同的内容
2,静态资源服务
webpack-dev-server使用了webpack-dev-middleware,改变了webpack打包的输出地址,使用了memory-fs模块将打包资源输出到内存中;
基于内存中的文件,根据路径,express使用中间件static搭建了静态文件服务器;
监控:用chokidar来监视文件变化, server的内部维护的有一个socket集合
3,单页面应用路由对应
使用了包connect-history-api-fallback,该包也是一个express中间件,用来对资源重定向
对于配置:
historyApiFallback可以配置为true和对象
var server = new WebpackDevServer(compiler, {
//contentBase:path.resolve(__dirname,"/build"),
publicPath: config.output.publicPath,
// hot: true,
headers: {
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
},
historyApiFallback: true,
stats: {
colors: true // 用颜色标识
},
// progress: true,
port: 9000,
overlay: {
warnings: true,
errors: true
},
noInfo: false
});
server.listen(9000, 'localhost', (err, res) => {
if (err) {
return console.log(err)
}
console.log('webpack 9000已启动...')
});
4,contentBase,contentBasePublicPath, transportMode等默认参数的设置
在utils/normalizeOptions.js文件中设置:
源码:
function normalizeOptions(compiler, options) {
// Setup default value
options.contentBase =
options.contentBase !== undefined ? options.contentBase : process.cwd();
// Setup default value
options.contentBasePublicPath = options.contentBasePublicPath || '/';
// normalize transportMode option
if (options.transportMode === undefined) {
options.transportMode = {
server: 'sockjs',
client: 'sockjs',
};
} else {
switch (typeof options.transportMode) {
case 'string':
options.transportMode = {
server: options.transportMode,
client: options.transportMode,
};
break;
// if not a string, it is an object
default:
options.transportMode.server = options.transportMode.server || 'sockjs';
options.transportMode.client = options.transportMode.client || 'sockjs';
}
}
if (!options.watchOptions) {
options.watchOptions = {};
}
}
上面代码设置了webpack-dev-server一些重要参数,了解默认参数,有助于了解options选项的含义
5,比较重要的options.publicPath
publicPath在源码中有两个地方用到
分别是:routes.js,createConfig
部分源码:
if (!options.publicPath) {
// eslint-disable-next-line
options.publicPath =
(firstWpOpt.output && firstWpOpt.output.publicPath) || '';
if (
!isAbsoluteUrl(String(options.publicPath)) &&
options.publicPath[0] !== '/'
) {
options.publicPath = `/${options.publicPath}`;
}
}
app.get('/webpack-dev-server', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.write(
'<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>'
);
const outputPath = middleware.getFilenameFromUrl(options.publicPath || '/');
const filesystem = middleware.fileSystem;
writeDirectory(options.publicPath || '/', outputPath);
res.end('</body></html>');
function writeDirectory(baseUrl, basePath) {
const content = filesystem.readdirSync(basePath);
res.write('<ul>');
content.forEach((item) => {
const p = `${basePath}/${item}`;
if (filesystem.statSync(p).isFile()) {
res.write(`<li><a href="${baseUrl + item}">${item}</a></li>`);
if (/\.js$/.test(item)) {
const html = item.substr(0, item.length - 3);
const containerHref = baseUrl + html;
const magicHtmlHref =
baseUrl.replace(
// eslint-disable-next-line
/(^(https?:\/\/[^\/]+)?\/)/,
'$1webpack-dev-server/'
) + html;
res.write(
`<li><a href="${containerHref}">${html}</a>` +
` (magic html for ${item}) (<a href="${magicHtmlHref}">webpack-dev-server</a>)` +
`</li>`
);
}
} else {
res.write(`<li>${item}<br>`);
writeDirectory(`${baseUrl + item}/`, p);
res.write('</li>');
}
});
res.write('</ul>');
}
});
publicPath指定了静态资源, html文件的路径
6,webpack配置中的output.publicPath和webpack-dev-server中的publicPath的区别
output.publicPath是指插入到html文件中静态资源路径的统一前缀
实例:
output: {
path: path.resolve(__dirname, '../build'),
publicPath: 'http://localhost:9000/a',
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js',
hotUpdateChunkFilename: '[id].[hash].hot-update.js',
hotUpdateMainFilename: '[hash].hot-update.json'
},
这时候在浏览器中:
webpack-dev-server中的publicPath规定了静态资源的路径,比如:
new WebpackDevServer(compiler, {
//contentBase:path.resolve(__dirname,"/build"),
publicPath:'http://localhost:9000/abc/',
// hot: true,
headers: {
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
}
}
这时候的静态资源路径统一前缀是/abc,比如要访问a.js文件,就需要访问:
http://localhost:9000/abc/js/a.js
所以上面两种publicPath的配置是不合理的,会出现html中的资源访问不到静态资源,所以两种publicPath应该设置一致,比如可以设置:
new WebpackDevServer(compiler, {
//contentBase:path.resolve(__dirname,"/build"),
publicPath: config.output.publicPath,
// hot: true,
}
7,多文件配置实例,主要原理是资源重定向
var server = new WebpackDevServer(compiler, {
//contentBase:path.resolve(__dirname,"/build"),
publicPath: config.output.publicPath,
// hot: true,
headers: {
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
},
historyApiFallback: {
rewrites:[
{from:/^\/a/,to:'/a.html'}, //以根‘/a’开始,以根‘/’结尾的请求,重定向到‘a.html’
{from:/^\/b/,to:'/b.html'}, //以‘/b’开始的请求,重定向到‘b.html’
{from:/./,to:'/views/404.html'} //不匹配上面的任意除了换行符之外的请求,重定向到‘404.html’
]
},
stats: {
colors: true // 用颜色标识
},
// progress: true,
port: 9000,
overlay: {
warnings: true,
errors: true
},
noInfo: false
});
server.listen(9000, 'localhost', (err, res) => {
if (err) {
return console.log(err)
}
console.log('webpack 9000已启动...')
});