webpack搭建服務(wù)端項(xiàng)目
參考文章: https://juejin.im/post/5cb1aabdf265da037b6101d3
前言
最近有如下一些零零碎碎的小需求,總結(jié)起來(lái),都是頁(yè)面偏向于展示,與用戶交互較少跛锌。因此,選擇搭建了一個(gè)多頁(yè)面的服務(wù)端項(xiàng)目栓霜。
- 低版本app用戶展示的升級(jí)頁(yè)面淤年,
- 以及其他需要作為app中一些用來(lái)幫忙實(shí)現(xiàn)部分功能的中間頁(yè)面
整個(gè)項(xiàng)目采用了webpack4 + express + ejs實(shí)現(xiàn)。
代碼地址:https://github.com/chestnut647/serverSideRendering
webpack構(gòu)建流程
由于是第一次脫離框架腳手架直接寫(xiě)webpack配置葵擎,花費(fèi)了些時(shí)間谅阿。整個(gè)思路如下:
- 構(gòu)思好項(xiàng)目的結(jié)構(gòu)
- 先簡(jiǎn)單配置entry 和output配置
- 需要支持es6【模塊化】,配置babel-loader
- 需要輸出html文件并直接js的自動(dòng)引入酬滤,引入html-loader loader以及html-webpack-plugin插件
- 不支持ejs語(yǔ)法中include語(yǔ)法签餐,使用ejs-html-loader
- 項(xiàng)目沒(méi)有熱更新,引入webpack-dev-middleware和webpack-hot-middleware
開(kāi)始構(gòu)建
介紹構(gòu)建過(guò)程中的一些重點(diǎn)盯串,項(xiàng)目初始架構(gòu)如下所示:
build內(nèi)文件如下氯檐,config.json用于存放一些webpack使用的常量配置
配置js入口
js入口配置,在配置entry的時(shí)候体捏,由于項(xiàng)目后續(xù)可能還會(huì)增加新的需求冠摄,不能每增加一次需求頁(yè)面,就要在webpack中新增entry几缭,因此采用了glob.sync來(lái)讀取js文件夾下的所有js文件河泳。
同時(shí)在使用html-webpack-plugin時(shí)我們也是使用glob.sync來(lái)避免每次都需要新增的問(wèn)題。
entry: (function(fileLists) {
let entryObj = {};
const basePath = resolve(__dirname, "../source/public/javascripts/main/");
fileLists.map((filePath) => {
const temp = filePath.split(basePath),
filename = temp[temp.length - 1].split('/').join('_').slice(1).split('.')[0];
entryObj[filename] = filePath;
});
return entryObj;
})(glob.sync(resolve(__dirname, "../source/public/javascripts/main/_*/_.js"))),
output: {
path: resolve(__dirname, `../${CONFIG.DIRC.DIST}`),
filename: `${CONFIG.DIRC.SCRIPT}/[name].bundle.js`
}
配置熱更新
使用webpack-dev-middleware配合webpack-hot-middleware來(lái)實(shí)現(xiàn)奏司。
- webpack-dev-middleware 用以實(shí)現(xiàn)文件變動(dòng)自動(dòng)編譯
- webpack-hot-middleware實(shí)現(xiàn)模塊熱替換
webpack-dev-middleware
首先我們判斷當(dāng)前是否為開(kāi)發(fā)環(huán)境乔询,非開(kāi)發(fā)環(huán)境的時(shí)候直接使用express方式啟動(dòng)項(xiàng)目。
當(dāng)為開(kāi)發(fā)環(huán)境時(shí)韵洋,我們采用webpack-dev-middleware【參數(shù)webpack.config.js生成的compile】生成的eppress中間件竿刁,訪問(wèn)的頁(yè)面的時(shí)候就會(huì)經(jīng)過(guò)webpack-dev-middleware,根據(jù)webpack.config.js里的配置搪缨,當(dāng)js文件變動(dòng)的時(shí)候可以自動(dòng)編譯食拜。
if(isDev) {
app.use(webpackDevMiddleware(compile, {
publicPath: webpackDevConfig.output.publicPath
}))
app.use(webpackDevConfig.output.publicPath, express.static(path.join(__dirname, 'source')))
} else {
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'dist/views'));
app.use(virtualDirctory, express.static(path.join(__dirname, 'dist')));
}
使用自動(dòng)編譯打包webpack-dev-middleware帶來(lái)了一個(gè)問(wèn)題:直接source目錄下的views文件夾來(lái)直接設(shè)置views是無(wú)法達(dá)到預(yù)期效果的。因?yàn)閟ource下的是沒(méi)有打包的ejs模板副编,并沒(méi)有注入項(xiàng)目中的js负甸、css。因此需要重寫(xiě)render方法
原來(lái)使用方式為:
router.get('/test1', function(req, res, next) {
res.render('test1', {
title: 'test1'
});
})
重寫(xiě)一個(gè)render方法:
當(dāng)在開(kāi)發(fā)環(huán)境下痹届,直接通過(guò)axios去獲取打包出來(lái)的ejs文件內(nèi)容呻待,再通過(guò)ejs.render渲染出來(lái)。
function render(res, filename, data) {
if(isDev) {
const localPath = `http://localhost:${packageConfig.config.port}${CONFIG.PATH.PUBLIC_PATH}/${CONFIG.DIRC.VIEW}/${filename}.ejs`;
axios.get(localPath)
.then(fileRes => {
const html = ejs.render(fileRes.data, data);
res.send(html)
})
return;
}
res.render(filename, data);
}
webpack-hot-middleware
通過(guò)上述配置队腐,可以發(fā)現(xiàn)蚕捉,當(dāng)對(duì)入口文件中的js或者css進(jìn)行修改后,webpack就會(huì)進(jìn)行自動(dòng)編譯柴淘,但是想要獲取最新代碼迫淹,還是需要手動(dòng)刷新瀏覽器秘通,引入webpack-hot-middleware
app.use(webpackHotMiddleware(compile, {
publicPath: webpackDevConfig.output.publicPath,
}))
同時(shí)修改webpack文件中entry入口配置
entry: (function(fileLists) {
let entryObj = {};
const basePath = resolve(__dirname, "../source/public/javascripts/main/");
fileLists.map((filePath) => {
const temp = filePath.split(basePath),
filename = temp[temp.length - 1].split('/').join('_').slice(1).split('.')[0];
// entryObj[filename] = filePath; 在開(kāi)發(fā)環(huán)境需要將webpack-hot-middleware添入到入口文件里
entryObj[filename] = isDev ? ['webpack-hot-middleware/client?noInfo=true&reload=true', filePath] :filePath;
});
return entryObj;
})(glob.sync(resolve(__dirname, "../source/public/javascripts/main/_*/_.js")))
通過(guò)這兩步配置后,修改js文件以及css文件瀏覽器會(huì)自動(dòng)更新敛熬。但是修改ejs模板文件肺稀,瀏覽器還是不能夠自動(dòng)更新。
在ejs模版文件的入口应民,增加如下代碼话原。
- 在js中直接require模版文件,當(dāng)ejs修改之后瑞妇,webpack就會(huì)將其視為需要熱更新的一部分
- 進(jìn)行模塊熱替換稿静,重新獲取打包后文件的內(nèi)容,替換到當(dāng)前頁(yè)面innerHtml上辕狰。
if(process.env.NODE_ENV === 'development') {
require('raw-loader!@views/test1.ejs')
}
if(module.hot) {
module.hot.accept();
module.hot.dispose(() => {
const axios = require('axios');
const href = window.location.href
axios.get(href).then(res => {
const template = res.data
document.body.innerHTML = template
}).catch(e => {
console.error(e)
})
})
}
當(dāng)入口頁(yè)面非常多的時(shí)候改备,你需要每個(gè)都手動(dòng)添加上述代碼,過(guò)于復(fù)雜蔓倍,寫(xiě)一個(gè)簡(jiǎn)單的loader悬钳,用以給每個(gè)入口j文件增加一段上述函數(shù)。
/**
* 給項(xiàng)目提供views的實(shí)時(shí)更新
* 在js文件中增加 enable hot updates of view, [filename] 的注釋即可
*/
module.exports = function (resource) {
const reg = /enable hot updates of view,[\s]*([^\s]+)/i;
const matchRes = resource.match(reg);
if(matchRes) {
const filename = matchRes[1];
console.log(`給文件${filename}.js添加ejs熱更新代碼`);
return resource + `
if(process.env.NODE_ENV === 'development') {
require('raw-loader!@views/${filename}.ejs')
}
if(module.hot) {
module.hot.accept();
module.hot.dispose(() => {
const axios = require('axios');
const href = window.location.href
axios.get(href).then(res => {
const template = res.data
document.body.innerHTML = template
}).catch(e => {
console.error(e)
})
})
}`
}
return resource;
}
同時(shí)修改webpack中的js文件的loader
{
test: /.js$/,
use: [
'babel-loader',
resolve(__dirname, 'auto-update-ejs-loader') // 添加增加熱更新文件的loader
],
exclude: /node_modules/
}
結(jié)語(yǔ)
除去上述的功能偶翅,代碼的完整配置里在生產(chǎn)環(huán)境下也包含了提取css默勾,splitchunk等功能【鬯可以直接下載代碼運(yùn)行起來(lái)~~