[前端工具]webpack2如何與現有網站和jquery結合使用
前言
之前有發一篇關於webpack2的入門教學,不過那一篇其實大多數是在講解語法,我相信對於略懂webpack,而且也知道webpack要幹什麼用的讀者來說,那篇文章應該可以了解一些比較基礎的配置和變化了,但是當我們在學習一個新工具的時候,一定會想說我應該怎麼運用這些東西呢?我該在什麼情境下使用呢?使用這個工具相對目前來說又有什麼優勢呢?所以這一篇想用大部份都是上一篇的說明,來示例一些情境和範例,當然除了是分享給各位之外,也是我自己腦補的一些情境,而且在這篇打算使用的是大家都熟悉的jquery,而不讓大部份的人感覺webpack是那些先進的前端框架在用的,其實jquery善用webpack也是可以達到很多的幫助的,而本篇要使用的會是.net的web form來做示例。
導覽
- 起手式準備
- 加入babelrc,使用es6以上的語法來寫javascript
- 修改打包的策略,拆成多支js
- import scss來寫scss的語法,並使用webpack打包
- import image,並使用data url的方式,節省request數量
- 加上sourcemap,已方便追蹤原始碼
- 用npm設定環境變數,切換開發和正式環境
- 在切換開發和正式環境的時候,指定常數給開發程式使用
- 相對於原本.net提供的bundle的優勢
- 結論
這次範例是web form,所以我會先建立package.json、webpack.config.js、app/index.js,但是之前webpack入門的部份講過的,這次就不會再花時間多寫了,當然針對上次沒講的,該補上註解說明的都會補上,如果有興趣的人,或許可以再去之前分享的那一篇了解一下(https://dotblogs.com.tw/kinanson/2017/06/11/124206),下面則是我web form的資料結構,圈起來的部份就是我個人為了webpack而新增的檔案,不過要特別注意一下,當我們修改了webpack.config或package.json的時候,我們都需要把cmd先關閉再重新執行,才會真的跑新的建置過程哦。
package.json
{
"name": "webpack.webform",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack --watch"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^2.6.1"
}
}
webpack.config.js
const path = require('path'),
webpack = require('webpack');
const webpackConfig = {
entry: {
app: './app/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/'
},
resolve: {
extensions: ['.js'],
}
};
module.exports = webpackConfig
app/index.js
$(function () {
$('#app').html('<div>hello world</div>');
})
Deafult.aspx
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="webpack.webform._Default" %>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<div id="app"></div> //jquery會取#app來改內容
</asp:Content>
Site.Master
然後打上npm run dev之後,應該就可以看到webpack開始監控了
畫面結果
我們可以試試修改index.js的文字內容,應該可以看到修改後重整,畫面都會即時更新了,這時候我們讀取的已經是dist/app.js的javascript內容了
加入babelrc,使用es6以上的語法來寫javascript
先新增一支.babelrc在根目錄下
{
"presets": [
["env", { "modules": false }],
"stage-2"
],
"plugins": ["transform-runtime"]
}
安裝babel用到的相關package
npm i babel-core babel-loader babel-plugin-transform-runtime babel-preset-env babel-preset-stage-2 --D
我們來修改一下webpack.config.js,以開始能支援es6的export和import吧
const path = require('path'),
webpack = require('webpack');
const webpackConfig = {
entry: {
app: './app/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',//js需要經過babel的轉譯
include: [path.join(__dirname, 'app')]
}
]
},
resolve: {
extensions: ['.js'],
}
};
module.exports = webpackConfig
新增一支app/ajaxData.js,來模擬假設是ajax來的資料
export default {
list: [
{ id: 1, name: 'anson', gender: 'body' },
{ id: 2, name: 'coco', gender: 'girl' }
]
};
app/index.js
import ajaxData from './ajaxData';
$(function(){
let $app = $('#app');
let iteratorList = `
<table class ="table">
<tr class="table-title">
<th>id</td>
<th>name</td>
<th>gender</td>
</tr>
`;
ajaxData.list.forEach(item=> {
iteratorList += `
<tr>
<td>${item.id}</td>
<td>${item.name}</td>
<td>${item.gender}</td>
</tr>
`
});
iteratorList += '</table>';
$app.html(iteratorList);
});
結果
由於export和import在chrome和ie11目前都還不能實現,以此範例兩個瀏覽器都能跑,證實了babel已經生效,如果你需要支持更新的api的話,可以使用babel polyfill,當然選擇lodash也是一種很好的做法。
以我們目前的打包方式,假設現在開始要修改about.aspx了,所以我先在app底下新增about.js,然後來摸擬一樣有個id為app的div
about.aspx
<%@ Page Title="About" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="About.aspx.cs" Inherits="webpack.webform.About" %>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<div id="app"></div>
</asp:Content>
app/about.js
$(function () {
$('#app').html('hello about');
})
webpack.config.js
entry: {
app: ['./app/index.js','./app/about.js']//兩支轉譯後會變成一支app.js
},
結果
從這邊可以看到,我們原本首頁應該是類似table的內容,但是現在卻都變成一樣的hello about,原因是因為我們是取app的方式來塞值,並最後結合成為一支dist/app.js,除非我們在每個頁面都能明確的為dom取一個獨一無二的命名,不然要結合成一支可能就容易造成入口的js檔(app/index.js and app/about.js)有全局汙染的狀況。
還有另一種不好的情況,因為我們網站勢必會越來越大,隨著存活的時間越來越長,如果我們把所有的js都打包成一支的話,好處是第一次戴入後就會快取(但要視客戶瀏覽器有沒有關閉快取的功能),不過有一個缺點是我們最後的js一定會非常肥非常大包,所以如果在舊有網站對於我來說比較好的打包策略,應該是每個頁面都有每個頁面自己的js檔,然後打包的方式是針對我們import用到的js檔,會自行打包相關用到的js,接著就再來動手實作吧,其實也非常的簡單,我們只要把webpack.config.js裡的entry修改一下就行了。
webpack.config.js
如果這段語法不了解在幹什麼的話,可以參考(https://dotblogs.com.tw/kinanson/2017/06/11/124206#2)
entry: {
app: ['./app/index.js'],
about: ['./app/about.js']
},
然後我們就要把Site.Master引用的app.js拿掉,在各個頁面使用自己的js檔,最後結果就會正常了
import scss來寫scss的語法,並使用webpack打包
scss相對css的優勢,我就不多說了,同樣的我們透過webpack很容易就直接可以轉成css,並且可以從js的入口點決定有哪些css是屬於這頁的部份,先來安裝一下相對應的package,這邊我會同時安裝css和sass的部份
npm i css-loader style-loader sass-loader node-sass --D
上面的安裝注意一下,筆者在不同電腦安裝過程式發生錯誤"Cannot read property 'find' of undefined",導致無法安裝node-sass,如果有遇到類似的錯誤,請先執行npm cache verify,然後再安裝就能成功了(至少筆者是成功了,筆者的npm是5.0.1版的,而且我發現5.0.1版有點怪怪的,所以我直接更新到5.0.3版,一些靈異現象就都正常了)
接著來修改一下webpack.config.js
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.join(__dirname, 'app')]
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"]
}
]
},
新增一支app/index.scss
.table-title{
color:white;
background-color:cadetblue;
}
app/index.js的第一行import進scss的檔案
import './index.scss';
結果應該會如圖下
然後我們輸出檔案並不會有css,因為他會把css都打包成js的方式,如果我們希望把css分開出來放的話,那我們就得裝個plugin
npm i extract-text-webpack-plugin --D
再修改一下webpack.config.js
const path = require('path'),
webpack = require('webpack'),
ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCss = new ExtractTextPlugin('[name].css'); //隨著import的檔名輸出css檔案
const webpackConfig = {
entry: {
index: ['./app/index.js'],
about: ['./app/about.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.join(__dirname, 'app')]
},
{
test: /\.css$/,
use: extractCss.extract([ "css-loader"])//包裝loader以便輸出css,style-loader在此就要拿掉了,因為我們不需要在js裡面使用css了,不拿掉的話會出錯
},
{
test: /\.scss$/,
use: extractCss.extract(["css-loader", "sass-loader"])//包裝loader以便輸出css,style-loader在此就要拿掉了,因為我們不需要在js裡面使用css了,不拿掉的話會出錯
}
]
},
resolve: {
extensions: ['.js'],
},
plugins: [
extractCss //放進此plugins
]
};
module.exports = webpackConfig
Default.aspx
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="webpack.webform._Default" %>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<link href="dist/index.css" rel="stylesheet" />
<div id="app"></div>
<script src="dist/index.js"></script>
</asp:Content>
重新npm run dev之後,應該能看到我們多了一支index.css出來了,下面是我目前的檔案結構圖示
import image,並使用data url的方式,節省request數量
在webpack一樣可以在js裡面import圖檔或font等等,或者也可以直接寫在css裡面,最後webpack都會幫你打包起來,先安裝一下相關loader
npm i file-loader url-loader --D
先修改一下webpack.config.js的部份
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.join(__dirname, 'app')]
},
{
test: /\.css$/,
use: extractCss.extract(["css-loader"])
},
{
test: /\.scss$/,
use: extractCss.extract(["css-loader", "sass-loader"])
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[ext]' //小於10000byte的話,直接使用data url的方式,而不會下載檔案
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[ext]'
}
}
]
},
app/index.js
import './index.scss';
import ajaxData from './ajaxData';
import manPng from './img/man.png';
$(function(){
let $app = $('#app');
let iteratorList = `
<table class ="table">
<tr class="table-title">
<th>id</td>
<th>name</td>
<th>gender</td>
</tr>
`;
ajaxData.list.forEach(item=> {
iteratorList += `
<tr>
<td>${item.id}</td>
<td>${item.name}</td>
<td>${getGender(item.gender)}</td>
</tr>
`
});
//多增加新的方法,來決定要回傳哪張圖片,這邊也可以定義import的方式,manPng像是我上面定義的變數
function getGender(gender){
if(gender==='body'){
return `<img src="${manPng}"/>`
}
return `<img src="${require('./img/woman.png')}"/>`
}
iteratorList += '</table>';
$app.html(iteratorList);
});
因為我的圖片只有1kb和2kb,所以就不會輸出圖片,只會直接內含在js裡面,而結果如下
其實在之前已經講過,也沒什麼大學問,要另外壓出sourcemap,只需要在devtools加上sourcemap的類型就可以了,不懂的話請查(https://dotblogs.com.tw/kinanson/2017/06/11/124206#5)
webpack.config.js
devtool: '#cheap-module-eval-source-map'
之前也有針對這個議題講過了,但其實webpack有提供了watch -p來提供我們打包時,順便就幫我們壓縮了,接下來我們需要處理的就是比如sourcemap的種類,之前的文章分享在npm是用set的方式在設定env,但是因為這個set的指令是windows用的,但mac就沒辦法這樣子用,所以我們如果這樣設定的方式,當有些member是用mac在開發的時候,clone下來就會直接出錯了,所以我們也需要把這個設置換用通用的方式來處理。
那就一個議題一個議題來說明吧,首先我們如果想要在任何環境都能正常的切換環境,有一個cross-env可以幫上我們的忙,先安裝一下
npm i cross-env --D
接著修改一下package.json,特別注意一下script的build部份用cross-env的方式來切換環境,用webpack -p的方式,直接幫我們壓縮javascript和css
{
"name": "webpack.webform",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "cross-env NODE_ENV=dev webpack --watch",
"build": "cross-env NODE_ENV=prod webpack -p"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.0.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.5.2",
"babel-preset-stage-2": "^6.24.1",
"cross-env": "^5.0.1",
"css-loader": "^0.28.4",
"extract-text-webpack-plugin": "^2.1.2",
"file-loader": "^0.11.2",
"node-sass": "^4.5.3",
"sass-loader": "^6.0.6",
"style-loader": "^0.18.2",
"url-loader": "^0.5.9",
"webpack": "^2.6.1"
}
}
webpack.config.js
const path = require('path'),
webpack = require('webpack'),
rimraf = require('rimraf'),
ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCss = new ExtractTextPlugin('[name].css');
const webpackConfig = {
entry: {
index: ['./app/index.js'],
about: ['./app/about.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.join(__dirname, 'app')]
},
{
test: /\.css$/,
use: extractCss.extract(
{
loader: "css-loader",
options: { sourceMap: true }
}
)
},
{
test: /\.scss$/,
use: extractCss.extract({
use: [
{
loader: "css-loader",
options: { sourceMap: true }
}, {
loader: "sass-loader",
options: { sourceMap: true }//為scss打包的時候也輸出.map檔
}]
})
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[ext]'
}
}
]
},
resolve: {
extensions: ['.js'],
},
devtool: '#cheap-module-eval-source-map',
plugins: [
extractCss
]
};
switch (process.env.NODE_ENV) { //切換環境決定配置
case 'prod':
rimraf(path.join(__dirname, 'dist'), () => console.log('success remove'));
webpackConfig.devtool = "#source-map";
break;
}
module.exports = webpackConfig;
當我們執行npm run build的時候,應該會在dist看到下面的檔案
結果
其實這種需求常常發生,當我們在開發環境和qat環境甚至正式環境,所面臨到的web api應該網址都不一樣,那我們是否能在切換dev或prod的時候,順便設定一些常數給開發程式使用呢,其實有辦法的哦,我們可以使用DefinePlugin來設定常數,在最後切換環境的時候加進去plugins裡面
webpack.config.js
switch (process.env.NODE_ENV) {
case 'dev':
webpackConfig.plugins.push(new webpack.DefinePlugin({
'process.env': {
'API_URL': '"http://localhost"'
}
}));
break;
case 'prod':
rimraf(path.join(__dirname, 'dist'), () => console.log('success remove'));
webpackConfig.devtool = "#source-map";
webpackConfig.plugins.push(new webpack.DefinePlugin({
'process.env': {
'API_URL': '"http://google"'
}
}));
break;
}
index.js的部份我就把process.env.API_URL給console.log出來,以確認我們是有設定成功的
console.log(process.env.API_URL);
結果
npm run dev的時候
npm run build
最後完整的webpack.config.js的程式碼
const path = require('path'),
webpack = require('webpack'),
rimraf = require('rimraf'),
ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCss = new ExtractTextPlugin('[name].css');
const webpackConfig = {
entry: {
index: ['./app/index.js'],
about: ['./app/about.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.join(__dirname, 'app')]
},
{
test: /\.css$/,
use: extractCss.extract(
{
loader: "css-loader",
options: { sourceMap: true }
}
)
},
{
test: /\.scss$/,
use: extractCss.extract({
use: [
{
loader: "css-loader",
options: { sourceMap: true }
}, {
loader: "sass-loader",
options: { sourceMap: true }
}]
})
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[ext]'
}
}
]
},
resolve: {
extensions: ['.js'],
},
devtool: '#cheap-module-eval-source-map',
plugins: [
extractCss
]
};
switch (process.env.NODE_ENV) {
case 'dev':
webpackConfig.plugins.push(new webpack.DefinePlugin({
'process.env': {
'API_URL': '"http://localhost"'
}
}));
break;
case 'prod':
rimraf(path.join(__dirname, 'dist'), () => console.log('success remove'));
webpackConfig.devtool = "#source-map";
webpackConfig.plugins.push(new webpack.DefinePlugin({
'process.env': {
'API_URL': '"http://google"'
}
}));
break;
}
module.exports = webpackConfig;
相信有在寫.net的人,在打包前端程式碼的時候,最多人使用的還是.net的BundleConfig.cs來做壓縮和結合檔案的處理,那webpack相對帶來了什麼優勢呢?
- webpack不需要重新建置,但bundle.cs需要(雖然有方法可以不用,不過如果你改了c#的程式碼,就一定得要重新建置)
- webpack可以方便的使用scss或處理data uri的問題,bundle.cs不行(我有點久沒用了,有錯請指正)
- webpack可以使用最先進的es6甚至以上的語法,或者要使用typescript通通沒有問題,而且可以自動處理瀏覽器相容問題,bundle.cs不能使用babel來轉換語法
當然能用不一定代表要用,各位公司不管是team leader或是developer都有自己心中一把尺,雖然webpack有相對的不少好處,但適不適合公司就留給讀者自行評估了。
終於寫完了,希望筆者用心的模擬情境,讀者也會有耐心的看完,其實webpack還有非常多功能和議題可以講,比如在修改程式碼之前先使用eslint幫我們檢查語法,甚至是css autoprefixer,或者是當檔案過大無法使用data uri的時候,可以在轉過來的時候順便對圖片做壓縮等等等,不過這一切的一切細節,都交給讀者自行優化和研究了,筆者深入研究webpack的原意,就是要完全摸透vue cli的一些配置,所以之後應該會再針對vue cli的webpack設置寫文章筆記,而在此我也附上這篇文章的原始碼,供各位可以自行研究囉,如果認為筆者有任何錯誤的話,也請多多指導囉。