Webpack在”多页“开发中遇到的问题
在开发时我们经常使用Webpack官方提供的webpack-dev-server
插件。我们只需要通过一个入口main.js
和入口页面HTML,用webpack-dev-server
就能够提供热更新“Live Reload”以及热替换“Hot Module Replacement”(即HMR) ,这很方便,但是在实际项目中我们遇到了更复杂的场景。
举例来说,我们现在的一个项目中不仅仅只是一个SPA应用了,它可能是由多个SPA应用构成的一个项目,每个SPA应用可能会由不同的人维护。因此它会有多个入口JS和入口HTML。由于没有提供唯一的入口js和html,仅仅使用webpack-dev-server
的方案就行不通了。对于这种”多页应用“的项目,最好的方案是在开发时能通过路由切换到对应的SPA应用下,即对应的入口JS和HTML下,为此我们需要一些小技巧。
解决方案
其实在使用Webpack以前其实我们不会有这种烦恼,也许这就是“螺旋式上升”的必经之路。总之,我一开始能想到的方案有以下三种。
- 在开发模式下通过Gulp监听文件变化,然后直接使用Webpack打包出文件,用Gulp-Server处理路由。
- 任然使用webpack-dev-server,通过proxy代理另一个Server处理路由。
- 只起一个Server + WebpackMiddleware 在保留热更新和热替换的基础上,增加多路由。
三种方法各有利弊。在此我们选择第三种方式,通过独立Server我们能够更方便的处理Mock中Post请求以及Prox等问题,自由度更大。
图片中的
multientry-dev-server
就是接下来我们要创造的server。
Webpack Dev\Hot Middleware
官方提供的webpack-dev-server
也只是一个用Express起的Server而已,其中使用了webpack-dev-middleware
和 webpack-hot-middleware
作为中间件提供Hot Module Replacement/Hot Reloading
能力。Webpack Hot Middleware 必须要搭配Webpack Dev Middleware才能使用,因此同样的我们也可以用Express启动一个有热替换功能的服务器,webpack-dev-server
只是做了一个简单的封装而已。
var http = require('http');
var express = require('express');
var path = require('path');
var webpackMiddleware = require('webpack-dev-middleware');
var webpackHotMiddleware = require('webpack-hot-middleware');
通过下面的代码建立Webpack的实例以及使用中间件
var app = express();
var compiler = webpack(webpackConfig);
var middleware = webpackMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
});
app.use(middleware);
app.use(webpackHotMiddleware(compiler));
通过路由获取内存中的Webapck打包文件
使用webpack中间件打包并不会真正的生成文件,它会把文件载入到内存中。
为了能通过路由指定跳转到对应的入口JS和HTML,我们在需在项目中做一些约定。假设项目入口为apps
目录,该目录下的每一个子目录对应一个SPA应用,在子目录中需要通过一个package.json
指定该SPA应用的入口JS和这个SPA应用的其他信息,比如名字和子应用负责人等。
package.json文件格式类似如下:
{
"name": "app1",
"main": "./main.js",
"author": "左伦"
}
middleware提供了middleware.fileSystem.readFileSync
方法读取内存中的文件,文件的地址就是在webpack中配置的输出地址。
app.get('/:appName', function (req, res) {
var result = '';
var htmlPath = path.join(__dirname, webpackConfig.output.path + req.params.appName + '/index.html');
console.log(htmlPath);
try {
result = middleware.fileSystem
.readFileSync(htmlPath);
} catch (err) {
result = err.toString();
}
res.write(result);
res.end();
});
OK, 至此便可以通过路由指定到对应App的入口。
Mock数据以及“首页”
由于是独立启动的Server,我们可以很方便的在Server中指定任意目录作为我们的静态目录,同时处理好对应的Post请求。
// 静态资源,Mock GET请求
app.use(express.static(path.join(__dirname, '../')));
// Mock POST请求
app.post('/api/*', function (req, res) {
res.sendFile(path.join(__dirname, '../api', req.params[0]));
});
当越来越多的子App在项目中后,通过手动在浏览器中输入路由再进行跳转会显得十分麻烦。因此可以在Server中新增一个“首页”,列出当前项目下的所有子应用以及开发时的对应路由,具体实现并不难,通过遍历目录下每个应用中的Packge.json即可,最后效果如下:
终于不用每次在浏览器中敲地址了..
到目前为止,已经成功解决了Webpack在“多页“应用下的开发问题,接下来是时候更进一步了。
使用target
指定入口应用
之前说到Webpack中间件在构建时会把文件都读取到内存中,但是当我们的项目越来越大的时候,项目下会有越来越多的子应用,这就造成了另一问题。有时我们只是在开发某一个子App下的代码,但是Webpack每次都会把整个项目打包进内存,非常浪费资源。因此这里我们可以在webpack的配置文件中做点文章,想办法只打包我们需要的目录下的JS。
在执行npm start
的时候,通过在命令行里用
target=appName1,appName2 npm start
其中appName为你想要启动的应用名称(名称在package.json中定义),此时Webpack配置中的Entry只会包含target指定应用名下的入口JS和HTML,大大缩短了Webpack启动时间并且减少了内存占用。这个想法最初是在团队的另一位师兄的代码中看到的,十分巧妙,关键代码如下:
var targetEntries = process.env.target;
targetEntries = targetEntries ? targetEntries.split(',') : '';
targetEntries.forEach(function (value) {
console.log('应用: ', value);
entryPath = path.join(viewsDir, value);
entryJson = fse.readJsonSync(path.join(entryPath, '/package.json'));
entryMap[value] = [path.resolve(path.join(entryPath, entryJson.main))];
var appName = entryJson.name;
var tplPath = path.join(entryPath, '/index.html');
var conf = {
template: tplPath,
filename: path.join(appName, 'index.html'),
inject: 'body',
chunks: [appName]
};
htmlPluginsArr.push(new HtmlWebpackPlugin(conf));
});
_.extend(webpackConfig, {
entry: entryMap,
output: {
path: path.join(__dirname, '../build/'),
filename: '[name]/[name].min.js'
},
plugins: [
new ExtractTextPlugin('[name]/[name].css'),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_console: true
}
})
].concat(htmlPluginsArr)
});
通过这样一个简单的服务,我们完全不用再使用Webpack-Dev-Server了,甚至我们也可以封装成一个相似的plugin。整个问题的思路和解决方案到此为止咯,如果大家有更好的想法可以补充。
文章首发于alisec-ued ,个人博客地址。