之前只知道webpack很強大,但是一直沒有深入學習過,這次從頭看了一下教程,然後從0開始搭建了一個多入口網站的開發腳手架,期間遇到過很多問題,所以有心整理一下,希望能給大家一點幫助。
多HTML網站使用webpack的必要性
假如我們接到這樣一個任務,開發一個簡單的官網,比如只有十幾個html頁面。專案很簡單,我們沒有必要使用什麼大型框架,但是如果只是傳統的寫幾個html、js和css,肯定會遇到這幾個問題:
- 網站導航和底部通欄是每個頁面都共有的,如何實現複用?如果不復用,那麼有改動的時候就要改n個頁面,未免太傻
- 如何在更改後強制清空使用者快取?我們不可能要求使用者手動去清除瀏覽器快取,那樣太傻
- 我想使用ES6進行js的開發,如何解決瀏覽器相容性問題?
- 我想使用less進行css樣式開發,如何轉換?
可以看出,沒有自動化打包工具的加入,這些問題我們是很難解決的,因此使用webpack勢在必行。
要實現的目標
看到這裡,可能有的同學就急了,別廢話,感覺進入正題吧,不!我們先定目標!無論做什麼事情,都要先定目標,而不是幹到哪裡算哪裡,這樣是不會有大的提升的,正是在邁向目標的路上克服各種問題,我們才有進步,在進行這個腳手架搭建之前,我希望它是這樣的
- 能夠打包成多個html檔案和js檔案,即支援多入口
- 檔名稱都要帶上hash值,解決快取問題
- 能夠複用網站的頭部導航欄和底部通欄
- 通過採用less進行樣式的編寫
- 能夠支援ES6開發
- 用起來要方便,增加頁面不需要手動去更改webpack的入口設定,希望能夠根據目錄下的檔案自動配置
- 不希望通過js動態插入css樣式,這樣會造成頁面閃爍,希望html中直接引入css地址,就像平時開發那樣
- 能夠實時看到開發的效果
- build能夠對程式碼進行壓縮
好了,目標定了,開工
目錄結構
別急,我們先來捋一捋目錄,彆著急寫程式碼,一個好的目錄,能讓我們思路清晰,我的目錄結構如下
+ config //環境變數配置檔案,開發模式和生產模式使用不同的環境變數,比如介面地址,開發環境用的介面域名是http://a.com,生產環境使用的是http://b.com
- dev.env.js //本地開發變數
- prod.env.js //生產環境變數
+ src
+ css //自己的less元件或者第三方css庫
+ component //自己元件的less
+ lib //第三方的css庫,比如bootstrap
+ html //html程式碼,主要是一些模板,如頭部導航,底部通欄,側邊欄
+ tpl //模板檔案
+ img //圖片檔案
+ js //自己的js元件庫或者第三方js庫
+ mod //自己的js元件放這裡
+ lib //第三方js庫
+ page //頁面檔案
+ index //這個根據自己情況設定,有的頁面相關性強,可以放到一個資料夾下,比如一個user資料夾,可以放個人中心的所有頁面
- index.html //每個頁面都要有一個html
- index.js //每個頁面都要有一個js,名稱和html的名稱保持一致
- index.less //每個頁面都要有一個同名less檔案
+ test
- test.html
- test.js
- test.less
+ webpack //webpack的配置檔案
- dev-server.js //開發服務設定,可以通過localhost訪問頁面,頁面的實時編譯
- webpack.common.js //開發環境和生產環境通用配置
- webpack.dev.js //開發環境特有的配置
- webpack.prod.js //生產環境特有的配置
首先是config目錄,目前我主要放一些環境變數,就是開發環境和生產環境所不同的變數,比如介面地址,我們開發的時候,用本地的api介面地址,而打包的時候,要換成生產環境api地址。
webpack目錄存放webpack的配置檔案,其中開發和生產通用配置 放到webpack.common.js中,開發特有配置放到webpack.dev.js中,生產特有配置放到webpack.prod.js中。
src是我們開發的主目錄,其中page目錄放置我們的頁面檔案,這裡可能和平時有所不同,我把每個頁面用到的html、js和less檔案放到了一起,有的同學可能把所有html放到一個目錄下,js放到一個目錄下,但是這樣存在一個問題,每次改動頁面,都要去翻目錄,非常的不方便,我們應該把這種高度相關的檔案放到一起,而提取的各種css元件或js元件可以和頁面分開放置。
github地址:https://github.com/501351981/…
多入口配置
webpack支援多入口,即給定多個入口js檔案,可以輸出多個js檔案,那麼html怎麼辦呢?我希望開發過程是這樣的,我在html中設定標題、SEO等資訊,編寫HTML內容程式碼,webpack把相關的js檔案自動插入到html底部就行,可以的,這需要用到html-webpack-plugin 外掛,可以通過呼叫html模板檔案打包最終html。
安裝html-webpack-plugin
npm install --save-dev html-webpack-plugin
webpack.common.js中多入口配置
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
module.exports={
entry:[
index:`../src/page/index/index.js`,
test:`../src/page/test/test.js`
],
output: {
filename: `[name].[hash].js`, //輸出名稱後面跟雜湊值,解決快取問題
path: path.resolve(__dirname,`../dist`)
},
....
plugins: [
new HtmlWebpackPlugin({
filename: `index.html`,
template: `../src/page/index/index.html`,
chunks: [`index`],
})
new HtmlWebpackPlugin({
filename: `test.html`,
template: `../src/page/test/test.html`,
chunks: [`test`],
})
]
}
這樣設定存在一個問題,每次新增一個頁面,我就要到這裡新增一下,未免很麻煩,我們其實可以通過讀取 src/page下的js檔案,自動加入入口配置;讀取 src/page下的所有html檔案,自動呼叫new HtmlWebpackPlugin進行例項化。
讀取目錄下所有檔名,我們需要引入glob,先安裝
npm install --save-dev glob
改進後的配置
const glob = require(`glob`)
const CleanWebpackPlugin = require(`clean-webpack-plugin`);
//多入口js的配置,讀取src/page下所有的js檔案
function entries() {
let jsDir = path.resolve(__dirname, `../src/page`)
let entryFiles = glob.sync(jsDir + `/**/*.js`)
let map = {};
for (let i = 0; i < entryFiles.length; i++) {
let filePath = entryFiles[i];
let filename = filePath.substring(filePath.lastIndexOf(`/`) + 1, filePath.lastIndexOf(`.`));
map[filename] = filePath;
}
return map;
}
//讀取多個html模板,進行外掛例項化
function newHtmlWebpackPlugins(){
let jsDir = path.resolve(__dirname, `../src/page`)
let htmls = glob.sync(jsDir + `/**/*.html`)
let plugins=[]
for (let i = 0; i < htmls.length; i++) {
let filePath = htmls[i];
let filename_no_extension = filePath.substring(filePath.lastIndexOf(`/`) + 1, filePath.lastIndexOf(`.`));
let filename=filename_no_extension.concat(`.html`)
plugins.push(new HtmlWebpackPlugin({
filename: filename,
template: filePath,
chunks: [filename_no_extension],
}))
}
return plugins
}
module.exports={
entry:entries(),
output: {
filename: `[name].[hash].js`,
path: path.resolve(__dirname,`../dist`)
},
....
plugins: [
...newHtmlWebpackPlugins()
]
}
好了,現在新增頁面不需要更改webpack配置了,只需要重新執行一下 npm run start即可
共有頭部和底部的複用
頭部導航和底部通欄我們各個頁面都是一樣的,因此需要引入,那麼html中怎麼引入另一個html呢,這需要用到raw-loader 或 html-withimg-loader
安裝raw-loader,raw-loader可以載入檔案原始內容(utf-8格式)
npm install --save-dev raw-loader
//目錄結構
+ src
+ html
+ tpl
- navbar.html //共用的頭部導航
- footer.html //共用的底部導航
+ page //頁面檔案
+ index
- index.html
+ test
- test.html
我們在index.html中可以這麼引用導航和底部通欄
<!--引入通用的導航部分-->
<%=require(`raw-loader!../../html/tpl/navbar.html`)%>
<!--頁面的正式內容在這裡-->
<div id="app">
<p>首頁的內容在這裡</p>
</div>
<!--引入通用的底部欄-->
<%=require(`raw-loader!../../html/tpl/footer.html`)%>
最初我在查詢解決方案的時候,看到文章推薦使用raw-loader,但是發現這樣存在一個問題,就是導航中無法引用本地的圖片,比如導航中引用一個logo圖片,是找不到的,因為我們打包的時候也會對圖片進行處理,後面新增hash值,直接寫圖片路徑是不行的,後來我改用 html-withimg-loader解決了
安裝html-withimg-loader,顧名思義,這個外掛可以載入帶有圖片的html
npm install --save-dev html-withimg-loader
<!--引入通用的導航部分-->
<%=require(`html-withimg-loader!../../html/tpl/navbar.html`)%>
<!--頁面的正式內容在這裡-->
<div id="app">
<p>首頁的內容在這裡</p>
</div>
<!--引入通用的底部欄-->
<%=require(`html-withimg-loader!../../html/tpl/footer.html`)%>
順便提一句,html中引用圖片地址是需要這樣寫的,需要通過require才行,簡單的填寫圖片地址是不行的
<img src="${require(`../../img/react.png`)}" width="50" height="50">
支援ES6編寫js
相信大家現在都已經學過ES6了,可是鑑於瀏覽器的相容性,還沒法隨心所欲的用,需要外掛支援,我們首先安裝
npm install --save-dev babel-loader babel-core babel-preset-env
新增webpack配置
webpack.common.js,我們只對src目錄下的js進行轉換
{
test: /.js$/,
use: {
loader: `babel-loader`
},
include: path.resolve(__dirname,`../src`)
},
同時在專案目錄下新增一個名為.babelrc的檔案,對babel進行設定,支援佔有率大於1%的瀏覽器的最近2個版本
{
"presets": [
["env",{
"targets": {
"browsers": ["> 1%", "last 2 versions"]
}
}]
],
}
babel只是將ES6語法轉為ES5的語法,比如箭頭函式轉為function(){},但是對一些ES6特有的功能沒有轉換,比如new Map(),打包之後還是new Map(),我們還需要再安裝一個外掛,完成這個轉換工作。
npm install --save-dev babel-plugin-transform-runtime
更改.babelrc檔案
{
"presets": [
["env",{
"targets": {
"browsers": ["> 1%", "last 2 versions"]
}
}]
],
"plugins": ["transform-runtime"] //引入外掛
}
現在可以放心大膽的使用ES6了
使用Less編寫樣式
首先還是安裝相關外掛
npm install --save-dev less less-loader css-loader style-loader
webpack.common.js配置
{
test: /.css$/,
use:["style-loader","css-loader","less-loader"]
},
在index.js檔案中,我們就可以這樣引入less檔案了
import `./index.less`
打包之後,執行html頁面,index.js會動態把css樣式插入到html頁面,這樣會造成一個問題,剛載入html的時候是一個樣式,js插入css樣式後是另一個樣式,造成頁面閃爍一下,體驗不好(技術也要追求使用者體驗啊,不光是產品經理的事)。這有兩個解決方案吧,第一個就是在JS未載入完成之前,顯示一個loading動畫,把整個頁面遮蓋住,第二個就是把css檔案路徑打包進html中,不要通過js動態新增,我選用的第二個方案。
我們要把less檔案打包到一個css檔案中,需要用到外掛extract-text-webpack-plugin
npm install --save-dev extract-text-webpack-plugin
webpack.common.js
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports={
rules: [
{
test: /.less$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader!less-loader"
})
},
]
}
plugins: [
new ExtractTextPlugin("[name].[hash:8].css"),
]
這樣打包之後的html中會引入css檔案,類似這樣
<link href="index.5eb2501d.css" rel="stylesheet">
webpack配置
實際在我從0開始搭建的過程中,是先進行webpack這塊的配置的,之所以放到最後是不想影響主幹內容,下面我們也簡單介紹一下我的webpack配置。
webpack官方推薦不寫重複的配置,即把本地和生產環境共用的配置放到一個檔案,然後通過merge進行合併
webpack.dev.js
const webpack = require(`webpack`);
const merge = require(`webpack-merge`);
const common = require(`./webpack.common`);
var OpenBrowserPlugin = require(`open-browser-webpack-plugin`);
const env=require("../config/dev.env")
module.exports=merge(common,{
mode:"development",
devtool: `inline-source-map`,
plugins:[
new webpack.DefinePlugin({
`process.env`: env
}),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
new OpenBrowserPlugin({ url: `http://localhost:5000` })
],
})
我們可以看到,通過webpack-merge外掛,將共用配置webpack.common.js和開發的配置進行合併
new webpack.DefinePlugin({
`process.env`: env
}),
DefinePlugin定義了全域性變數process.env
new OpenBrowserPlugin({ url: `http://localhost:5000` })
這個外掛是為了在我們允許npm run start後,自動開啟頁面http://localhost:5000,避免每次都手動開啟。
webpack-dev-server 為我們提供了一個簡單的 web 伺服器,並且能夠實時重新載入,讓我們可以實時看到開發結果,關於web伺服器的配置,我放到了dev-server.js中
const webpackDevServer = require(`webpack-dev-server`);
const webpack = require(`webpack`);
const config = require(`./webpack.dev`);
const options = {
contentBase: `./dist`,
hot: true,
host: `localhost`,
};
webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);
server.listen(5000, `localhost`, () => {
console.log(`dev server listening on port 5000`);
});
在package.json中,我們新增兩個指令碼
"scripts": {
"start": "node webpack/dev-server.js",
"build": "npx webpack --config webpack/webpack.prod.js",
},
這樣我們就可以在命令列輸入兩個命令
npm run start :進入開發模式
npm run build:打包生產環境程式碼
好了,基本上把我做的這個腳手架介紹完了,實際要理解還需要自己去試,看是一回事,做出來又是另一回事,給別人講明白那就更不容易了,前端路漫漫,大家努力吧。
github地址:https://github.com/501351981/…
這個腳手架還不完善,不過基本夠用了,後面我還會再做幾個腳手架,比如結合vue進行多頁面開發或移動端H5開發,有興趣可以持續關注。