選擇 webpack4 仍是 fis3 爲 jQuery 多頁面工程引入 ES6

工程描述

現有工程的特色:

  1. 是多頁面的工程,頁面的JS經過Ajax拿到數據後,使用JQuery渲染到DOM中;
  2. 沒有模塊化的機制,每一個JS裏僅僅是經過IIFE方法不污染全局做用域而已;
  3. 不一樣頁面的JS中有不少的冗餘代碼;

要達到的目標:

  1. 可以經過引入模塊化的方法,解決代碼的冗餘,提升編碼的效率;
  2. 可以支持ES6的特性,方便之後的使用;
  3. 可否實現Vue單文件那樣的組件化

解決方案:

    對於目標1和2, 若是僅僅爲了支持模塊化的話,能夠考慮使用AMD(RequireJS)或者CMD(SeaJS)這兩種方案;但,咱們仍是但願可以使用ES6的新特性,好比箭頭函數,promise,async/await等頗有用的特性,因此,模塊化的機制就採用了es6的import/export。javascript

    對於目標3,Vue單文件那樣的組件化,本質上是將一個組件對應的js/css/html放在一塊兒,這樣能夠很方便的進行處理和複用;宏觀上講,每一個頁面均可以經過一個個組件搭建起來,這種複用的方式,很簡潔很強大。直接使用Vue的重構成本過高,因此,咱們但願可以採用相似的思想實現,這個問題咱們後面會有探討。css

    使用了es6特性後,源代碼就不可以直接在瀏覽器上運行了,須要通過構建工具的處理,好比fis3或者webpack;這樣的話,每次都須要編譯,開發的時候會不會不方便呢?這個問題在後面咱們會有方法來解決。html

嘗試fis3

現有工程用的構建工具就是fis1.9,不過僅用了混淆代碼和壓縮的功能;不過咱們發現fis3的功能已經足夠實現咱們的需求了;因此,首先嚐試的就是fis3.前端

fis3的原理

fis3的構建過程能夠歸納爲三步:java

  1. 掃描工程目錄拿到文件,並初始化爲一個文件對象列表;
  2. 首先,對每一個文件進行 單文件編譯(編譯的時候會讀取用戶對該類文件設置的文件屬性以及插件進行編譯);
  3. 而後,獲取用戶設置的package插件,進行 打包處理

對文件屬性的設置,以及插件的使用,都在fis-conf.js中配置就行;node

fis3其實天生就很是適合多頁面工程的構建,由於fis3是不會修改你的項目結構,除非你fis3你打包的時候須要合併某些文件;用一句話來解釋就是,fis3從全部文件上流過,根據文件對象上的設置對文件進行編譯,全部文件編譯完後,根據設置的打包配置進行打包。jquery

fis3的進一步的解釋可參考1與2webpack

工程目錄結構

.
├── assets
│   ├── images
│   │   ├── adv_course.png
│   │   ├── adv_realscreen.png
│   │   └── app_banner.jpg
│   └── scss
│       ├── a.scss
│       └── variables.scss
├── components // 組件
│   └── table 
│       ├── table.es6
│       └── table.scss
├── libs //庫文件
│   └── jquery-1.11.1.js
├── mock //用於開發的時候,設置代理
│   └── server.conf
├── modules //各個模塊
│   ├── data.es6
│   ├── date.es6
│   └── text.es6
├── node_modules
├── pages
│   ├── pageA.es6 //模塊化的js文件
│   ├── pageB.es6
│   ├── pageC.es6
│   └── pageD.js //非模塊化的js文件
├── a.html
├── b.html
├── c.html
├── fis-conf.js
├── mod.js
├── package.json
└── README.md
複製代碼

代碼地址:fis3方案demo
git

fis3方案的解釋

配置文件fis-conf.jses6

fis.match('*.es6', {//對.es6後綴的文件,須要用babel將es6的代碼轉換爲es5的
  parser: fis.plugin('babel-6.x', {
    plugins:['transform-runtime']
  }),
  rExt: '.js'
});

fis.match('*.scss', {
  parser: fis.plugin('node-sass', {//將scss文件解析爲css
    // options...
  }),
  rExt: '.css'
})
fis.match('*.{js,es,es6,jsx,ts,tsx}', {//能夠在js文件中require scss文件,有利於組件化
  preprocessor: fis.plugin('js-require-css')
})

  
// 開啓模塊化開發
fis.match('/node_modules/**.js', {
  isMod: true,
  useSameNameRequire: true
});
fis.match('*.es6', {
  isMod: true
});

fis.hook('commonjs', {
  extList: ['.js', '.jsx', '.es6', '.es', '.ts', '.tsx']
});
fis.match('::package', {
  postpackager: fis.plugin('loader')
});
fis.unhook('components');
fis.hook('node_modules');複製代碼

三個目標的實現:

1. fis3是怎麼支持模塊化的?

fis.match('/node_modules/**.js', {
  isMod: true,
  useSameNameRequire: true
});
fis.match('*.es6', {
  isMod: true
});

fis.hook('commonjs', {
  extList: ['.js', '.jsx', '.es6', '.es', '.ts', '.tsx']
});
fis.unhook('components');
fis.hook('node_modules');複製代碼

上面的配置之因此有這麼多,是由於要對npm的node_modules模塊的支持,由於babel模塊是須要打包到線上的,而fis3不會自動對node_modules中的模塊進行處理,因此就須要fis3-hook-node_modules,可參考3

fis3編譯的時候,會把es6模塊文件用define函數進行包裹;同時把es6的import和export,變爲commonjs的require和exports;簡單來講,define函數就是把模塊和模塊ID(默認是路徑)進行映射,require就是經過模塊ID(也就是路徑)獲得對應的模塊。define的包裹效果以下:

define('modules/date.es6', function(require, exports, module) {

  "use strict";
  
  Object.defineProperty(exports, "__esModule", {
      value: true
  });
  var getDateStr = function getDateStr() {
      return new Date().toLocaleDateString();
  };
  
  exports.getDateStr = getDateStr;

});複製代碼

模塊化的支持還須要mod.js這個文件(第一個加載執行),由於require和define這兩個函數的定義就在mod.js中;

//a.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="/assets/scss/a.scss">
    
    <title>page a</title>
    <script type="text/javascript" src="./mod.js"></script>
</head>
<body>
    <p>
        <img src="/assets/images/adv_course.png">
        <button id="changePic">change pic</button>
    </p>
    <div class="bg-image"></div>
    <span id="span-a-date"></span>
    <span id="span-a-text"></span>
    <p id="span-d-text"></p>
    <button id="changeTextSync">change</button>
    <button id="changeTextWithPromise">change with promise</button>
    <button id="changeTextWithAsync">change with async</button>
    <button id="changeTableText">change table text</button>
    <div id="component-table"> </div>

    <script type="text/javascript" src="./libs/jquery-1.11.1.js"></script>
    <script type="text/javascript" src="./pages/pageD.js"></script>
    <script type="text/javascript">
        require('./pages/pageA')
    </script>
</body>
</html>複製代碼

fis3編譯a.html,經過解析require('./pages/pageA'),將相關的依賴模塊createScript插入a.html中,以下:

<script type="text/javascript" src="/mod.js"></script>
.....
<script type="text/javascript" src="/modules/date.js"></script>
<script type="text/javascript" src="/modules/text.js"></script>
<script type="text/javascript" src="/modules/data.js"></script>
<script type="text/javascript" src="/components/table/table.js"></script>
<script type="text/javascript" src="/pages/pageA.js"></script>
<script type="text/javascript" src="/libs/jquery-1.11.1.js"></script>
<script type="text/javascript" src="/pages/pageD.js"></script>
<script type="text/javascript">
    require('pages/pageA.es6')
</script>
複製代碼

當加載頁面的時候,會順序加載並執行各個模塊js,因爲模塊已經用define函數包裹了,因此執行模塊js文件的時候僅僅是把模塊ID<-->模塊函數factory;這種映射很快,不會卡頓後面的執行;

當最後執行require('./pages/pageA')的時候,全部依賴的模塊都已經映射好了(至關於存在內存中了),能夠直接require了。(至關於模擬出commonjs的同步加載的效果)

關於模塊化的理解,能夠見參考4

2. 怎麼支持es6?

es6的支持很簡單,只須要這麼配置就行

fis.match('*.es6', {//對.es6後綴的文件,須要用babel將es6的代碼轉換爲es5的
  parser: fis.plugin('babel-6.x', {
    plugins:['transform-runtime']
  }),
  rExt: '.js'
});複製代碼

遇到的問題是,不知道怎麼把dependencies的模塊一塊兒打包,這個上面已經提到了。

demo中,嘗試了箭頭函數、Promise、async/await這三個特性,都可以很好的支持。

3. 怎麼實現組件化呢?

在demo中,寫了個簡單table組件

// /components/table/table.es6,定義組件
require('./table.scss') // 組件的css直接在js中加載,很方便

export class MyTable {
    constructor (el) {
        this.el = el
    }
    setData (data) {
        this.data = data
        this.render()//數據更改的時候,從新渲染組件
    }
    render () {
        let data = this.data, cnt = $('.component-table.'+this.el)
        cnt.empty()
        cnt.append('table:'+data)
    }
}

// /pages/pageA.es6,使用組件
import {MyTable} from '/components/table/table.es6';
let table = new MyTable('pageA')
$('#changeTableText').click(()=>{
    let num = parseInt(Math.random()*1000)
    table.setData(num);
})

// pageA.html,組件的container
<div class="component-table pageA"> </div>複製代碼

組件的layout在組件內部寫好後,再render到對應的page的container中;固然組件的class應該得按照約定書寫;這樣的話,咱們就大概模擬了Vue的組件化思想。 

小結

fis3很是適合多頁面的構建,由於fis3不會改變你的文件結構,因此不須要像webpack那樣考慮不少的路徑問題;使用fis3進行重構的方案很簡單:須要使用模塊化的文件後綴爲.es6;新增components目錄支持組件化;其餘不須要改動,能夠在現有工程上逐步升級,能夠實現平滑的升級。

fis3有個缺點:沒有成熟的社區,如今用的人彷佛少了,有問題得本身擼,固然這個還能夠克服。

那麼使用fis3構建後,有沒有辦法讓咱們在開發的時候方便些呢?特別是api的代理,由於這樣能夠很方便前端調試。慶幸的是,fis3提供了server功能,咱們能夠經過fis3  server start在本地啓動一個web server,只要在/mock/server.conf中設置proxy代理,就能夠將api代理到你想獲取數據的服務器上,這種組合能夠很讓咱們很方便的調試,具體可參考5和6

嘗試webpack4

webpack算是如今最火熱的構建工具,因此咱們也想要嘗試下,看是否可以更方便的實現咱們的目標。

webpack原理

webpack的構建原理可大概分爲五步:

  1. 讀取配置,初始化Compiler對象,同時加載全部的plugins,調用Compiler的run方法;
  2. 從entry出發,調用配置的模塊loader進行編譯,再對模塊依賴的模塊進行編譯;
  3. 完成編譯後,獲得各個模塊間的依賴關係;
  4. 根據entry和模塊之間的依賴關係,組裝成一個個包含多個模塊的chunk,再把每一個chunk轉換成一個單獨的文件加入輸出列表;(這是修改輸出內容的最後機會)
  5. 肯定好輸出內容後,根據配置肯定輸出的路徑和文件名,把文件內容寫到文件系統中。

有幾個概念須要解釋下:

  1. loader,webpack把一切文件都當作模塊,而模塊的編譯就須要對應的loader;
  2. plugin,webpack會在特色的時間點廣播特定事件,plugin能夠監聽特定的事件後執行定製的邏輯,而後調用webpack提供的api改變webpack運行的結果;
  3. chunk,webpack經過引用關係逐個打包module,這些module就造成了一個Chunk,具體可見參考9

用一句話來解釋就是,webpack從配置的entry開始,根據loader和plugin編譯entry及其依賴,再根據輸出路徑獲得編譯後的文件;webpack的進一步理解,可參考7與8

工程目錄結構

目錄的結構與fis3方案的差很少,只是把html和對應的js文件放在了一個目錄下,方便webpack處理。

├── build
│   ├── utils.js
│   └── webpack.dev.conf.js //webpack配置文件
├── dist // build後的目錄
│   ├── css
│   │   └── a.71d83539237d40d5d190.css
│   ├── images
│   │   ├── adv_course.d31756acb1d985ffb7a8a9ae8e989497.png
│   │   ├── adv_realscreen.708b822d3ff896f2fc1fc938676235c8.png
│   │   └── app_banner.c8eb7e13e6a2684bfa60d56416a07782.jpg
│   ├── js
│   │   ├── a-c102f7fbc2aace2a3b95.js
│   │   ├── b-2b3d38d6e9b425cdc5c9.js
│   │   └── c-dfc151f9ec73442ad091.js
│   ├── libs
│   │   ├── jquery-1.11.1.js
│   │   └── pageD.js
│   ├── a.html
│   ├── a_m.html
│   ├── b.html
│   └── c.html
├── node_modules
├── src
│   ├── assets
│   │   ├── images
│   │   │   ├── adv_course.png
│   │   │   ├── adv_realscreen.png
│   │   │   └── app_banner.jpg
│   │   └── scss
│   │       └── variables.scss
│   ├── components // 組件
│   │   └── table
│   │       ├── table.es6
│   │       └── table.scss
│   ├── libs //不須要build的放這裏
│   │   ├── jquery-1.11.1.js
│   │   └── pageD.js
│   ├── modules // 模塊
│   │   ├── data.es6
│   │   ├── date.es6
│   │   └── text.es6
│   └── pages // 頁面
│       ├── a
│       │   ├── a.html
│       │   ├── a_m.html //與a.html公用一個pageA.es6
│       │   ├── a.scss
│       │   └── pageA.es6
│       ├── b
│       │   ├── b.html
│       │   └── pageB.es6
│       └── c
│           ├── c.html
│           └── pageC.es6
├── package.json
└── README.md複製代碼

代碼地址:webpack4方案demo

webpack方案的解釋

配置文件webpack.dev.conf.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); 
const CopyWebpackPlugin = require('copy-webpack-plugin'); 
const ExtractTextPlugin = require("extract-text-webpack-plugin");

const getEntries = ()=>{ // 多個入口依賴
    return {
        a: './src/pages/a/pageA.es6',
        b: './src/pages/b/pageB.es6',
        c: './src/pages/c/pageC.es6'
    }
}

let webpackCfg = {
    entry: getEntries(),
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/',
        filename: 'js/[name]-[chunkhash].js'
    },
    devServer: {
        index: 'a.html',
        contentBase: false,
        publicPath: '/',
        port: 8080,
        open: true,
        proxy: {
            '/stock/*': {
                target: 'https://guorn.com',
                changeOrigin: true
              }
        }
    },
    module: {
        rules: [
            {
                test: /\.html$/,
                use: {
                    loader: 'html-loader'
                }
            },
            {
                test: /\.scss$/,
                use: ExtractTextPlugin.extract({
                    fallback: "style-loader",
                    use: [
                        {
                            loader: "css-loader"
                        },
                        {
                            loader: "resolve-url-loader"
                        },
                        {
                            loader: "sass-loader",
                            options: {
                                sourceMap: true
                            }
                        }
                    ]
                })
            },
            {//es6的編譯
                test: /\.es6?$/,
                loader: 'babel-loader'
            },
            {
                test: /\.(png|jpe?g|gif|svg)$/i,
                use: [
                  {
                    loader: 'url-loader',
                    // 配置 url-loader 的可選項
                    options: {
                      // 限制 圖片大小 10000B,小於限制會將圖片轉換爲 base64格式
                      limit: 10000,
                      // 超出限制,建立的文件格式
                      // build/images/[圖片名].[hash].[圖片格式]
                      name: 'images/[name].[hash].[ext]'//utils.getAssetsPath('images/[name].[hash].[ext]')
                   }
                  }
                ]
            }
        ]
    },
    plugins: [
         //靜態資源輸出,libs下的文件不用編譯,完整輸出到dist/libs中
         new CopyWebpackPlugin([{
            from: path.resolve(__dirname, "../src/libs"),
            to: 'libs/',
            ignore: ['.*']
        }]),
        new ExtractTextPlugin('css/[name].[hash].css')
    ]
}

//生成多個頁面
var pages = [
    {
        filename: 'a.html',
        template: 'src/pages/a/a.html',
        chunks: 'a'
    },
    {
        filename: 'a_m.html',
        template: 'src/pages/a/a_m.html',
        chunks: 'a'
    },
    {
        filename: 'b.html',
        template: 'src/pages/b/b.html',
        chunks: 'b'
    },
    {
        filename: 'c.html',
        template: 'src/pages/c/c.html',
        chunks: 'c'
    }
]
pages.forEach(function(page) {
    var conf = {
        filename: page.filename, // 文件名,生成的html存放路徑,相對於path
        template: page.template, // html模板的路徑
        chunks: [page.chunks],
        inject: 'body', // //js插入的位置
        minify: { // 壓縮HTML文件
            removeComments: true, // 移除HTML中的註釋
            collapseWhitespace: false, // 刪除空白符與換行符
            removeAttributeQuotes: true
        },
    }
    webpackCfg.plugins.push(new HtmlWebpackPlugin(conf))
});
module.exports = webpackCfg;複製代碼

基本思路: 

  1. 對於不須要webpack處理的文件,好比libs/下的,使用CopyWebpackPlugin直接輸出;
  2. 對於有依賴的模塊,則須要使用webpack處理;因爲webpack是從entry開始解析以及相應的依賴,而後將之一塊兒打包爲對應的文件;因此,entry通常是js文件(由於js文件內可使用import/export來使用依賴模塊);那js對應的html怎麼辦呢?能夠經過HtmlWebpackPlugin將對應的js文件插入到html中。

webpack處理的時候會遇到各類路徑問題,可參考10

三個目標的實現:

1. webpack是怎麼支持模塊化的?

webpack也是按照commonjs的規範來處理模塊。

首先,webpack將entry所依賴的模塊,用函數包裹起來,放在數組中(至關因而 把模塊和模塊ID映射起來,這裏的模塊ID是數組的索引,這樣模塊已經存在內存中了,能夠直接調用了);而後,調用第一個模塊。具體確定沒這麼簡單,我就不班門弄斧了, 能夠參考11,講的蠻好的。

與fis3的模塊化處理方案相比,二者本質上是同樣的,比較大的不一樣是:fis3是把全部依賴模塊createScript插入html頁面,而後當頁面加載的時候調用define進行模塊ID與模塊的映射。

2. webpack是怎麼支持es6的?

只要按照以下配置就能夠了

webpack.dev.conf.js

{
	test: /\.es6?$/,
	loader: 'babel-loader'
},複製代碼

.babelrc

{
  "presets": [
    ["@babel/env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }]
    // "@babel/stage-2"
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}
複製代碼

因爲webpack是把依賴的模塊直接打包到入口文件中,因此不須要像fis3那樣單獨考慮處理node_modules目錄下的依賴模塊。

關於babel的理解,可參考12.  

3. 怎麼實現組件化?

組件化的思路與fis3方案中的差很少,可參考fis3方案中的介紹。

小結

無疑webpack很強大,社區也很成熟,不少問題都有解決方案,雖然更合適單頁面工程的構建,但也適用多頁面工程的構建。

webpack也有個webpack-dev-server提供了server功能,咱們能夠經過在本地啓動一個web server訪問頁面,同時也能夠設置proxy代理,將api代理到你想獲取數據的服務器上,開發仍是蠻方便的。

綜合考慮,因爲咱們須要兼容原有的項目,使用fis3構建的方案改動最小,三大目標也能夠實現,同時也能夠在兼容原有js的基礎上一步步引入es6,因此咱們最後用的是fis3方案。

參考

1. fis3的工做原

2. 從配置文件認識fis3

3. fis3 對npm的node_modules模塊的支持

4. 前端模塊化詳解

5. fis3 node-mock;

6. 前端工程化之數據代理;

7. webpack原理

8. webpack文檔; 

9. webpack理解chunk;

10. webpack中publicPath詳解;

11. 談談對模塊化的理解;

12. 一口氣了解babel;

相關文章
相關標籤/搜索