[前端工具]webpack2如何與現有網站和jquery結合使用

[前端工具]webpack2如何與現有網站和jquery結合使用

前言

之前有發一篇關於webpack2的入門教學,不過那一篇其實大多數是在講解語法,我相信對於略懂webpack,而且也知道webpack要幹什麼用的讀者來說,那篇文章應該可以了解一些比較基礎的配置和變化了,但是當我們在學習一個新工具的時候,一定會想說我應該怎麼運用這些東西呢?我該在什麼情境下使用呢?使用這個工具相對目前來說又有什麼優勢呢?所以這一篇想用大部份都是上一篇的說明,來示例一些情境和範例,當然除了是分享給各位之外,也是我自己腦補的一些情境,而且在這篇打算使用的是大家都熟悉的jquery,而不讓大部份的人感覺webpack是那些先進的前端框架在用的,其實jquery善用webpack也是可以達到很多的幫助的,而本篇要使用的會是.net的web form來做示例。

導覽

  1. 起手式準備
  2. 加入babelrc,使用es6以上的語法來寫javascript
  3. 修改打包的策略,拆成多支js
  4. import scss來寫scss的語法,並使用webpack打包
  5. import image,並使用data url的方式,節省request數量
  6. 加上sourcemap,已方便追蹤原始碼
  7. 用npm設定環境變數,切換開發和正式環境
  8. 在切換開發和正式環境的時候,指定常數給開發程式使用
  9. 相對於原本.net提供的bundle的優勢
  10. 結論

起手式準備

這次範例是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也是一種很好的做法。

修改打包的策略,拆成多支js

以我們目前的打包方式,假設現在開始要修改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,已方便追蹤原始碼

其實在之前已經講過,也沒什麼大學問,要另外壓出sourcemap,只需要在devtools加上sourcemap的類型就可以了,不懂的話請查(https://dotblogs.com.tw/kinanson/2017/06/11/124206#5)

webpack.config.js

devtool: '#cheap-module-eval-source-map'

用npm設定環境變數,切換開發和正式環境

之前也有針對這個議題講過了,但其實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提供的bundle的優勢

相信有在寫.net的人,在打包前端程式碼的時候,最多人使用的還是.net的BundleConfig.cs來做壓縮和結合檔案的處理,那webpack相對帶來了什麼優勢呢?

  1. webpack不需要重新建置,但bundle.cs需要(雖然有方法可以不用,不過如果你改了c#的程式碼,就一定得要重新建置)
  2. webpack可以方便的使用scss或處理data uri的問題,bundle.cs不行(我有點久沒用了,有錯請指正)
  3. webpack可以使用最先進的es6甚至以上的語法,或者要使用typescript通通沒有問題,而且可以自動處理瀏覽器相容問題,bundle.cs不能使用babel來轉換語法

當然能用不一定代表要用,各位公司不管是team leader或是developer都有自己心中一把尺,雖然webpack有相對的不少好處,但適不適合公司就留給讀者自行評估了。

結論

終於寫完了,希望筆者用心的模擬情境,讀者也會有耐心的看完,其實webpack還有非常多功能和議題可以講,比如在修改程式碼之前先使用eslint幫我們檢查語法,甚至是css autoprefixer,或者是當檔案過大無法使用data uri的時候,可以在轉過來的時候順便對圖片做壓縮等等等,不過這一切的一切細節,都交給讀者自行優化和研究了,筆者深入研究webpack的原意,就是要完全摸透vue cli的一些配置,所以之後應該會再針對vue cli的webpack設置寫文章筆記,而在此我也附上這篇文章的原始碼,供各位可以自行研究囉,如果認為筆者有任何錯誤的話,也請多多指導囉。

https://github.com/kinanson/webpack.webform/tree/master