【實戰(zhàn)】webpack4 + ejs + express 帶你擼一個多頁應(yīng)用項目架構(gòu)

最終解決方案

多頁應(yīng)用項目架構(gòu)最終解決方案發(fā)布啦~

【實戰(zhàn)】webpack4 + ejs + egg 多頁應(yīng)用項目最終解決方案

egg 版本是在這一版本基礎(chǔ)上所作的升級雏节,服務(wù)端的健壯性和可擴展性都有所提高~ ,不過大部分細節(jié)這篇已經(jīng)完全介紹了,大家可以兩篇結(jié)合著一起看伴栓。

前言

GitHub 完整項目地址

最近接了一個公司官網(wǎng)的項目僚碎,需要 SEO 友好蔗坯,所以不能使用前端框架植榕,前端框架自帶的腳手架工具自然也幫不上啥忙幻馁。只好自己使用 webpack4 + ejs + express 脓钾,從頭搭建一個多頁應(yīng)用的項目架構(gòu)售睹。搭建過程中,遇到許多坑可训,然而網(wǎng)上的相關(guān)參考也是非常少昌妹,所以寫個博客記錄一下搭建過程以及注意事項。

“同構(gòu)”還是“服務(wù)端渲染”握截?

這篇文章發(fā)表后飞崖,有朋友在評論區(qū)問為什么不直接使用一些同構(gòu)的框架,比如 nextjs 或者 nuxtjs谨胞?這個問題可能也是我們開發(fā)的時候比較糾結(jié)的問題之一固歪,我說一下自己的想法。

同構(gòu)

其實所謂的“同構(gòu)”胯努,也只是在加載網(wǎng)頁第一屏的時候牢裳,使用了服務(wù)端渲染,由服務(wù)器解析 VDOM 生成真實 DOM 然后返回叶沛。等到網(wǎng)頁第一屏代碼加載完成蒲讯、前端框架接管瀏覽器的時候,后續(xù)的整個流程灰署,就已經(jīng)是客戶端渲染判帮,和服務(wù)端沒有關(guān)系了局嘁。

優(yōu)點

  • 能夠通過前端框架,以“組件”的方式組織頁面結(jié)構(gòu)晦墙,更加靈活导狡,復(fù)用性好
  • 背靠前端框架三巨頭,各種插件組件生態(tài)良好
  • 后續(xù)頁面切換流暢偎痛、用戶體驗好
  • 數(shù)據(jù)操作便捷旱捧,數(shù)據(jù)共享方便,適合復(fù)雜業(yè)務(wù)場景踩麦,或是需要數(shù)據(jù)共享的場景

缺點

  • 首屏加載時間慢枚赡,白屏?xí)r間長,會出現(xiàn)閃屏等情況(實際上是由于客戶端代碼又做了一次客戶端渲染導(dǎo)致)
  • 由服務(wù)端進行 VDOM 解析谓谦、構(gòu)建 DOM 有一定性能損耗贫橙,訪問量大的情況下可能成為服務(wù)端的性能瓶頸
  • 構(gòu)建流程復(fù)雜,構(gòu)建過程中出現(xiàn)問題反粥,排錯難度較高
  • 數(shù)據(jù)同步困難卢肃,涉及數(shù)據(jù)脫水注水等。

服務(wù)端渲染

傳統(tǒng)意義上的服務(wù)端渲染才顿,則是在整個網(wǎng)頁的生命周期內(nèi)莫湘,都由服務(wù)端直接生成靜態(tài)頁面返回給客戶端。

優(yōu)點

  • 構(gòu)建流程相對簡單郑气,出錯定位方便
  • 服務(wù)端性能開銷較小
  • 首屏加載快幅垮,白屏?xí)r間短

缺點

  • 前后端耦合度高,代碼不夠靈活尾组,復(fù)用困難
  • 數(shù)據(jù)操作復(fù)雜忙芒,需要開發(fā)人員手動介入到繁雜的 DOM 操作中,不適合密集型交互和大量數(shù)據(jù)操作
  • 前端代碼缺少框架約束和管理讳侨,容易寫出祖?zhèn)鞔a

取舍

綜合以上呵萨,當我們在面對一個需要考慮 SEO 的項目,什么時候選擇前后端同構(gòu)跨跨,什么時候選擇傳統(tǒng)的服務(wù)端渲染潮峦?

我認為:如果你的項目是一個 toC 的產(chǎn)品、需要考慮 SEO歹叮、涉及大量用戶交互及頻繁的需求變更跑杭,那么可能同構(gòu)更適合你。它能以組件的方式構(gòu)建你的項目咆耿,高度抽離和復(fù)用德谅,并且能夠支持一些在傳統(tǒng)服務(wù)端渲染情況下根本沒法做的功能,比如網(wǎng)頁云音樂網(wǎng)頁客戶端萨螺,切換頁面的時候還能保證歌曲不斷窄做,肯定使用了同構(gòu)愧驱。

而如果只是一個 toB 的中小型企業(yè)官網(wǎng),考慮 SEO 但是不涉及大量的用戶交互椭盏,一旦做完后期變動也不大组砚,那么可以考慮選擇傳統(tǒng)的服務(wù)端渲染,也就是下面文章提及的方法掏颊。

明確需求

在動手開發(fā)之前糟红,我們需要先明確這個項目的定位——公司官網(wǎng),一般來說乌叶,官網(wǎng)不會涉及大量的數(shù)據(jù)交互盆偿,比較偏向于數(shù)據(jù)展示。所以不用前端框架准浴,jquery 即可滿足需求事扭。但是考慮到 SEO 所以需要用到服務(wù)端渲染,就要使用模板語言(ejs)乐横,配合 node 來完成求橄。

根據(jù)以上信息,我們就可以確定打包腳本的基本功能葡公,先來簡單列個清單:

  1. 需要 webpack 來打包多頁應(yīng)用罐农,且不需要每次新增一個視圖文件都添加一個 HTMLWebpackPlugin 和重啟 server ,能做到 webpack 配置和文件名解耦匾南,盡量的自動化啃匿。
  2. 需要使用 ejs 模板語言編寫,能夠插入變量和外部 includes 文件蛆楞,最后運行 build 命令的時候能將通用模板文件(<meta>/<title>/<header>/<footer> 等)自動插入每個視圖文件對應(yīng)位置。
  3. 需要用到服務(wù)端渲染夹厌,所以開發(fā)環(huán)境要脫離 webpack 集成的 webpack-dev-server豹爹,能使用自己編寫的 node 代碼啟動服務(wù)。
  4. 擁有完善的 overlay 功能矛纹,可以像 webpack-dev-server 那樣集成漂亮的 overlay 屏幕報錯臂聋。
  5. 能監(jiān)聽文件變化,自動打包和重啟服務(wù)或南,最好能做到熱更新

開始構(gòu)建

先建立一個空項目孩等,由于需要自己編寫服務(wù)端代碼,所以我們需要多建一個 /server 文件夾采够,用來存放 express 的代碼肄方,搭建完成后,我們的項目結(jié)構(gòu)看起來是這樣蹬癌。
[圖片上傳失敗...(image-be0cc5-1603173382903)]

除此以外权她,我們需要初始化一些通用配置文件虹茶,包括:

  • .babelrc babel 配置文件
  • .gitignore git 忽略文件
  • .editorConfig 編輯器配置文件
  • .eslintrc.js eslint 配置文件
  • README.md 文件
  • package.json 文件

大的框架出來以后,我們開始編寫工程代碼隅要。

打包腳本

首先是編寫打包腳本蝴罪,在/build文件夾里新建幾個文件

  1. webpack.base.config.js,用來存放生產(chǎn)環(huán)境和開發(fā)環(huán)境通用的 webpack 配置
  2. webpack.dev.config.js用來存放開發(fā)環(huán)境的打包配置
  3. webpack.prod.config.js用來存放生產(chǎn)環(huán)境的打包配置
  4. config.json 用來存放一些配置常量步清,例如端口名要门,路徑名之類。

一般來說廓啊,webpack.base.config 文件里暂衡,放一些開發(fā)生產(chǎn)環(huán)境通用的配置,例如 output崖瞭、entry 以及一些 loader狂巢, 例如編譯ES6語法的 babel-loader、打包文件的 file-loader 等书聚。常用的 loader 的使用方式我們可以查看文檔 webpack loaders唧领,需要注意的是,這邊有個非常重要的 loader ———— ejs-html-loader

一般來說雌续,我們使用 html-loader 來對.html結(jié)尾的視圖文件做處理斩个,然后扔給 html-webpack-plugin生成對應(yīng)的文件,但是 html-loader 無法處理 ejs 模板語法中的 <% include ... %> 語法驯杜,會報錯受啥。然而在多頁應(yīng)用里,這個 include 的功能是必須的鸽心,不然每個視圖文件里都要手動去寫一份 header/footer 是什么感覺滚局。。顽频。所以我們需要再多配置一份 ejs-html-loader:

// webpack.base.config.js 部分代碼
module: {
    rules: [
        ...
        {
            test: /\.ejs$/,
            use: [
                {
                    loader: 'html-loader', // 使用 html-loader 處理圖片資源的引用
                    options: {
                        attrs: ['img:src', 'img:data-src']
                    }
                },
                {
                    loader: 'ejs-html-loader', // 使用 ejs-html-loader 處理 .ejs 文件的 includes 語法
                    options: {
                        production: process.env.ENV === 'production'
                    }
                }
            ]
        }
        ...
    ]
}

第一個坑繞過之后藤肢,第二個:

entry 入口要怎么寫?

記得之前公司的一個老項目糯景,五十幾個頁面嘁圈,五十幾個 entrynew HTMLwebpackPlugin() 一個文件展開來可以繞地球一圈。蟀淮。最住。這邊為了避免這種慘狀,寫一個方法怠惶,返回一個 entry 數(shù)組涨缚。

可以使用 glob 來處理這些文件,獲取文件名甚疟,當然同樣也可以使用原生 node 來實現(xiàn)仗岖。只要保證 JavaScript 文件名和視圖文件名相同即可逃延,比如,首頁的視圖文件名是 home.ejs,那么對應(yīng)的腳本文件名就要用同樣的名字 home.js 來命名轧拄,webpack 打包的時候會找到腳本文件入口揽祥,通過映射關(guān)系生成對應(yīng)視圖文件:

// webpack.base.config.js 部分代碼
const Webpack = require('Webpack')
const glob = require('glob')
const { resolve } = require('path')

// webpack 入口文件
const entry = ((filepathList) => {
    let entry = {}
    filepathList.forEach(filepath => {
        const list = filepath.split(/[\/|\/\/|\\|\\\\]/g) // 斜杠分割文件目錄
        const key = list[list.length - 1].replace(/\.js/g, '') // 拿到文件的 filename
        // 如果是開發(fā)環(huán)境,才需要引入 hot module
        entry[key] = process.env.NODE_ENV === 'development' ? [filepath, 'webpack-hot-middleware/client?reload=true'] : filepath
    })
    return entry
})(glob.sync(resolve(__dirname, '../src/js/*.js')))

module.exports = {
    entry,
    ...
}

HTMLWebpackPlugin 的配置也同理:

// webpack.base.config.js 部分代碼
...
plugins: [
    // 打包文件
    ...glob.sync(resolve(__dirname, '../src/tpls/*.ejs')).map((filepath, i) => {
        const tempList = filepath.split(/[\/|\/\/|\\|\\\\]/g)           // 斜杠分割文件目錄
        const filename = `views/${tempList[tempList.length - 1]}`       // 拿到文件的 filename
        const template = filepath                                       // 指定模板地址為對應(yīng)的 ejs 視圖文件路徑
        const fileChunk = filename.split('.')[0].split(/[\/|\/\/|\\|\\\\]/g).pop() // 獲取到對應(yīng)視圖文件的 chunkname
        const chunks = ['manifest', 'vendors', fileChunk]               // 組裝 chunks 數(shù)組
        return new HtmlWebpackPlugin({ filename, template, chunks })    // 返回 HtmlWebpackPlugin 實例
    })
]
...

編寫好 webpack.base.config.js 文件檩电,根據(jù)自己項目需求編寫好 webpack.dev.config.jswebpack.prod.config.js拄丰,使用 webpack-merge 將基礎(chǔ)配置和對應(yīng)環(huán)境下的配置合并。

webpack 其他的一些細節(jié)配置大家可以參考 webpack 中文網(wǎng)址

服務(wù)端

打包腳本編寫完成俐末,我們開始編寫服務(wù)料按,我們使用 express 來搭建服務(wù)。(由于是工程架構(gòu)演示卓箫,所以這個服務(wù)暫不涉及任何的數(shù)據(jù)庫的增刪改查载矿,只是包含基本的路由跳轉(zhuǎn))

server 簡單的結(jié)構(gòu)如下:
[圖片上傳失敗...(image-888708-1603173451542)]

服務(wù)端啟動文件

bin/server.js 啟動文件,作為服務(wù)的入口烹卒,需要同時啟動本地服務(wù)和 webpack 的開發(fā)時編譯闷盔。一般項目 webpack-dev-server 是寫在 package.json 里的,當你運行 npm run dev 的時候旅急,就在使用 webpack-dev-server 啟動開發(fā)服務(wù)逢勾,這個 webpack-dev-server 功能十分強大,不僅能一鍵啟動本地服務(wù)藐吮,還可以監(jiān)聽模塊溺拱,實時編譯。這邊我們使用 express + webpack-dev-middleware 也可以達到同樣的功能谣辞。

webpack-dev-middleware 可以理解為一個抽離出來的 webpack-dev-server迫摔,只是沒有啟動本地服務(wù)的功能,以及使用方式上略有改變潦闲。它相比于 webpack-dev-server 的靈活性在于攒菠,它以一個中間件的形式存在,允許開發(fā)者編寫自己的服務(wù)來使用它歉闰。

其實 webpack-dev-server 的內(nèi)部實現(xiàn)機制也是借助于 webpack-dev-middleware 和 express 有興趣的朋友可以去看一下。

以下是服務(wù)入口文件的部分代碼

// server/bin/server.js 文件代碼
const path = require('path')
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const { routerFactory } = require('../routes')
const isDev = process.env.NODE_ENV === 'development'
let app = express()
let webpackConfig = require('../../build/webpack.dev.config')
let compiler = webpack(webpackConfig)

// 開發(fā)環(huán)境下才需要啟用實時編譯和熱更新
if (isDev) {
    // 用 webpack-dev-middleware 啟動 webpack 編譯
    app.use(webpackDevMiddleware(compiler, {
        publicPath: webpackConfig.output.publicPath,
        overlay: true,
        hot: true
    }))
    
    // 使用 webpack-hot-middleware 支持熱更新
    app.use(webpackHotMiddleware(compiler, {
        publicPath: webpackConfig.output.publicPath,
        noInfo: true
    }))
}

// 添加靜態(tài)資源攔截轉(zhuǎn)發(fā)
app.use(webpackConfig.output.publicPath, express.static(path.resolve(__dirname, isDev ? '../../src' : '../../dist')))

// 構(gòu)造路由
routerFactory(app)

// 錯誤處理
app.use((err, req, res, next) => {
    res.status(err.status || 500)
    res.send(err.stack || 'Service Error')
})

app.listen(port, () => console.log(`development is listening on port 8888`))

服務(wù)端路由

路由的跳轉(zhuǎn)方式卓起,屬于整個工程中非常重要的一步和敬。不知道閱讀文章的朋友有沒有疑問,本地的視圖文件是 .ejs 后綴結(jié)尾的文件戏阅,瀏覽器只能識別 .html 后綴文件昼弟,這塊視圖數(shù)據(jù)的渲染是怎么做的? webpack-dev-middleware 打包出來的資源都是存在內(nèi)存中的奕筐,存儲在內(nèi)存中的資源文件舱痘,服務(wù)端要怎么獲缺渎狻?先來看具體的路由代碼芭逝,此處以首頁路由作為演示

// server/routs/home.js 文件
const ejs = require('ejs')
const { getTemplate } = require('../common/utils')

const homeRoute = function (app) {
    app.get('/', async (req, res, next) => {
        try {
            const template = await getTemplate('index.ejs') // 獲取 ejs 模板文件
            let html = ejs.render(template, { title: '首頁' })
            res.send(html)
        } catch (e) {
            next(e)
        }
    })
    app.get('/home', async (req, res, next) => {
        try {
            const template = await getTemplate('index.ejs') // 獲取 ejs 模板文件
            let html = ejs.render(template, { title: '首頁' })
            res.send(html)
        } catch (e) {
            next(e)
        }
    })
}

module.exports = homeRoute

可以看到關(guān)鍵點就在 getTemplate 這個方法塌碌,我們看看這個 getTemplate 做了咩

// server/common/utils.js 文件
const axios = require('axios')
const CONFIG = require('../../build/config')

function getTemplate (filename) {
    return new Promise((resolve, reject) => {
        axios.get(`http://localhost:8888/public/views/${filename}`) // 注意這個 'public' 公共資源前綴非常重要
        .then(res => {
            resolve(res.data)
        })
        .catch(reject)
    })
}

module.exports = {
    getTemplate
}

從上面代碼可以看到,路由中的做的非常重要的事情旬盯,就是直接用對應(yīng)視圖的 ejs 文件名台妆,去請求自身服務(wù),從而獲取到存在 webpack 緩存中的資源和數(shù)據(jù)胖翰。
通過這種方式拿到模板字符串后接剩,ejs 引擎會用數(shù)據(jù)渲染對應(yīng)變量,最終以 html 字符串的形式返回到瀏覽器進行渲染萨咳。
本地服務(wù)會以一個 publicPath 路徑前綴來標記靜態(tài)資源請求懊缺,如果服務(wù)接受到的請求是帶有 publicPath 前綴,就會被 /bin/server.js 中的靜態(tài)資源中間件攔截到培他,映射到對應(yīng)資源目錄鹃两,返回靜態(tài)資源,而這個 publicPath 就是 webpack 配置中的 output.publicPath

關(guān)于 webpack 的打包時緩存靶壮,我之前翻了很多地方都沒有找到很好的文檔和操作工具怔毛,這邊給大家推薦兩個鏈接

  1. Webpack Custom File Systems (webpack 自定義文件系統(tǒng)官方說明)
  2. memory-fs(獲取 webpack 編譯到內(nèi)存中的數(shù)據(jù))

客戶端

完成了服務(wù)端渲染、webpack 構(gòu)建配置后腾降,算是搞定了 80% 的工作量拣度,還有一些小細節(jié)需要注意,不然服務(wù)啟動起來還是會報錯螃壤。

webpack 編譯時的坑

這個坑就埋在客戶端的視圖文件里抗果,先來看看坑是什么:當我們使用 ejs 語法(<%= title %>)這種語法的時候,webpack 編譯就會報錯奸晴,說是 title is undefined

要解決這個問題冤馏,需要首先明白 webpack 編譯時的運行機制,它做了什么寄啼。我們知道逮光,webpack 內(nèi)部模板機制就是基于的 ejs,所以在我們服務(wù)端渲染之前墩划,也就是 webpack 的編譯階段涕刚,已經(jīng)執(zhí)行過了一次 ejs.render 了,這個時候乙帮,在 webpack 的配置文件里杜漠,我們是沒有傳遞過 title 這個變量的,所以編譯會報錯。那么要怎么寫才能識別呢驾茴?答案就在 ejs 的官方文檔
[圖片上傳失敗...(image-9734c4-1603173338896)]

從官網(wǎng)的介紹上可以看出盼樟,當我們使用 <%% 打頭的時候,會被轉(zhuǎn)義成 <% 字符串锈至,類似于 html 標簽的轉(zhuǎn)義晨缴,這樣才能避免 webpack 中自帶的 ejs 的錯誤識別,生成正確的 ejs 文件裹赴。所以以變量為例喜庞,在代碼中我們需要這樣寫: <%%= title %>
這樣,webpack 才能順利編譯完成棋返,將 compiler 繼續(xù)傳遞到 ejs-html-loader 這里

使用 html-loader 識別圖片資源

如果了解 html-loader 的朋友就知道延都,在項目中,我們之所以能夠在 html 中方便的寫 <img src="../static/imgs/XXX.png"> 這種圖片格式睛竣,還能被 webpack 正確識別晰房,離不開 html-loader 里的 attrs 配置項,
但是在 ejs-html-loader 里射沟,沒有提供這種方便的功能殊者,所以我們依舊要使用 html-loader 來對 html 中的圖片引用做處理,這邊需要注意 loader 的配置順序

// webpack.base.config.js 部分代碼
module: {
    rules: [
        ...
        {
            test: /\.ejs$/,
            use: [
                {
                    loader: 'html-loader', // 使用 html-loader 處理圖片資源的引用
                    options: {
                        attrs: ['img:src', 'img:data-src']
                    }
                },
                {
                    loader: 'ejs-html-loader', // 使用 ejs-html-loader 處理 .ejs 文件的 includes 語法
                    options: {
                        production: process.env.ENV === 'production'
                    }
                }
            ]
        }
        ...
    ]
}

配置熱更新

接下來是配置熱更新验夯,使用 webpack-dev-middleware 時的熱更新配置方式和 webpack-dev-server 略有不同猖吴,但是 webpack-dev-middleware 稍微簡單一點。webpack 打包多頁應(yīng)用配置熱更新挥转,一共四步:

  1. entry 入口里多寫一個 webpack-hot-middleware/client?reload=true 的入口文件
// webpack.base.config.js 部分代碼
// webpack 入口文件
const entry = ((filepathList) => {
    let entry = {}
    filepathList.forEach(filepath => {
        ...
        // 如果是開發(fā)環(huán)境海蔽,才需要引入 hot module
        entry[key] = process.env.NODE_ENV === 'development' ? [filepath, 'webpack-hot-middleware/client?reload=true'] : filepath
        ...
    })
    return entry
})(...)

module.exports = {
    entry,
    ...
}
  1. 在 webpack 的 plugins 里多寫三個 plugin:
    // webpack.dev.config.js 文件部分代碼
    plugins: [
    ...
    
    // OccurrenceOrderPlugin is needed for webpack 1.x only
    new Webpack.optimize.OccurrenceOrderPlugin(),
    new Webpack.HotModuleReplacementPlugin(),
    // Use NoErrorsPlugin for webpack 1.x
    new Webpack.NoEmitOnErrorsPlugin()
    
    ...
    ]
    
  2. bin/server.js 服務(wù)入口中引入 webpack-hot-middleware, 并將 webpack-dev-server 打包完成的 compilerwebpack-hot-middleware 包裝起來:
    // server/bin/server.js 文件
    let compiler = webpack(webpackConfig)
    
    // 用 webpack-dev-middleware 啟動 webpack 編譯
    app.use(webpackDevMiddleware(compiler, {
        publicPath: webpackConfig.output.publicPath,
        overlay: true,
        hot: true
    }))
    
    // 使用 webpack-hot-middleware 支持熱更新
    app.use(webpackHotMiddleware(compiler, {
        publicPath: webpackConfig.output.publicPath,
        reload: true,
        noInfo: true
    }))
    
  3. 在視圖對應(yīng)的 js 文件里加一段代碼:
    // src/js/index.js 文件
    if (module.hot) {
        module.hot.accept()
    }
    

關(guān)于 webpack-hot-middleware 的更多配置細節(jié),請看文檔

這邊需要注意的是:
1. 光是這么寫的話绑谣,webpack hot module 只能支持 JS 部分的修改党窜,如果需要支持樣式文件( css / less / sass ... )的 hot reload ,就不能使用 extract-text-webpack-plugin 將樣式文件剝離出去借宵,否則無法監(jiān)聽修改幌衣、實時刷新。

2. webpack hot module 原生是不支持 html 的熱替換的壤玫,但是很多開發(fā)者對于這塊的需求比較大豁护,于是我找了一個相對比較簡單的方法,來支持視圖文件的熱更新
需要在原有的代碼做一點修改欲间,先來看代碼:

// src/js/index.js 文件
import axios from 'axios'
// styles
import 'less/index.less'

const isDev = process.env.NODE_ENV === 'development'

// 在開發(fā)環(huán)境下择镇,使用 raw-loader 引入 ejs 模板文件,強制 webpack 將其視為需要熱更新的一部分 bundle
if (isDev) {
    require('raw-loader!../tpls/index.ejs')
}

...

if (module.hot) {
    module.hot.accept()
    /**
    * 監(jiān)聽 hot module 完成事件括改,重新從服務(wù)端獲取模板,替換掉原來的 document
    * 這種熱更新方式需要注意:
    * 1. 如果你在元素上之前綁定了事件,那么熱更新之后嘱能,這些事件可能會失效
    * 2. 如果事件在模塊卸載之前未銷毀吝梅,可能會導(dǎo)致內(nèi)存泄漏
    */
    module.hot.dispose(() => {
        const href = window.location.href
        axios.get(href).then(res => {
            const template = res.data
            document.body.innerHTML = template
        }).catch(e => {
            console.error(e)
        })
    })
}

// webpack.dev.config.js
plugins: [
    ...
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('development')
    })
    ...
]
// webpack.prod.config.js
plugins: [
    ...
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production')
    })
    ...
]

OK,如你所愿惹骂,現(xiàn)在視圖文件也支持熱更新啦苏携。????

webpack-hot-middleware 默認繼承了 overlay,所以當熱更新配置完成以后对粪,overlay 報錯功能也能正常使用了

1603173563(1).jpg

package.json 啟動腳本

最后來看一下 package.json 里的啟動腳本右冻,這邊沒啥難度,就直接上代碼了

    "scripts": {
        "clear": "rimraf dist",
        "server": "cross-env NODE_ENV=production node ./server/bin/server.js",
        "dev": "cross-env NODE_ENV=development nodemon --watch server ./server/bin/server.js",
        "build": "npm run clear && cross-env NODE_ENV=production webpack --env production --config ./build/webpack.prod.config.js",
        "test": "echo \"Error: no test specified\" && exit 1"
    }

當客戶端代碼變動時 webpack 會自動幫我們編譯重啟著拭,但是服務(wù)端的代碼變動卻不會實時刷新纱扭,這時需要用到 nodemon,設(shè)置好監(jiān)聽目錄以后儡遮,服務(wù)端的任何代碼修改就能被 nodemon 監(jiān)聽乳蛾,服務(wù)自動重啟,非常方便鄙币。

這邊也有一個小細節(jié)需要注意肃叶,nodemon --watch 最好指定監(jiān)聽服務(wù)端文件夾,因為畢竟只有服務(wù)端的代碼修改才需要重啟服務(wù)十嘿,不然默認監(jiān)聽整個根目錄因惭,寫個樣式都能重啟服務(wù),簡直要把人煩死绩衷。

總結(jié)

項目整體搭完后再回頭看蹦魔,還是有不少需要注意和值得學(xué)習(xí)的地方。雖然踩了不少坑唇聘,但也對其中的一些原理有了更深入的了解版姑。

得益于前端腳手架工具,讓我們能在大部分項目中一鍵生成項目的基礎(chǔ)配置迟郎,免去了很多工程搭建的煩惱剥险,但這種方便在造福了開發(fā)者的同時,卻也弱化了前端工程師的工程架構(gòu)能力∠苄ぃ現(xiàn)實中總有一些腳手架工具沒辦法的觸及到的業(yè)務(wù)場景表制,這時就需要開發(fā)者主動尋求解決方案,甚至自己動手構(gòu)建工程控乾,以獲得開發(fā)的最佳靈活性么介。

完整項目地址可以查看我的 GitHub ,喜歡的話給個 Star?? 蜕衡,多謝多謝~????

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末壤短,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌久脯,老刑警劉巖纳胧,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異帘撰,居然都是意外死亡跑慕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門摧找,熙熙樓的掌柜王于貴愁眉苦臉地迎上來核行,“玉大人,你說我怎么就攤上這事蹬耘≈パ” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵婆赠,是天一觀的道長绵脯。 經(jīng)常有香客問我,道長休里,這世上最難降的妖魔是什么蛆挫? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮妙黍,結(jié)果婚禮上悴侵,老公的妹妹穿的比我還像新娘。我一直安慰自己拭嫁,他們只是感情好可免,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著做粤,像睡著了一般浇借。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怕品,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天妇垢,我揣著相機與錄音,去河邊找鬼肉康。 笑死闯估,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的吼和。 我是一名探鬼主播涨薪,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炫乓!你這毒婦竟也來了刚夺?” 一聲冷哼從身側(cè)響起献丑,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎光督,沒想到半個月后阳距,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡结借,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了卒茬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片船老。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖圃酵,靈堂內(nèi)的尸體忽然破棺而出柳畔,到底是詐尸還是另有隱情,我是刑警寧澤郭赐,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布薪韩,位于F島的核電站,受9級特大地震影響捌锭,放射性物質(zhì)發(fā)生泄漏俘陷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一观谦、第九天 我趴在偏房一處隱蔽的房頂上張望拉盾。 院中可真熱鬧,春花似錦豁状、人聲如沸捉偏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夭禽。三九已至,卻和暖如春谊路,著一層夾襖步出監(jiān)牢的瞬間讹躯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工凶异, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蜀撑,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓剩彬,卻偏偏與公主長得像酷麦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喉恋,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345