选择 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;

相关文章
相关标签/搜索