工程描述
现有工程的特点:
- 是多页面的工程,页面的JS通过Ajax拿到数据后,使用JQuery渲染到DOM中;
- 没有模块化的机制,每个JS里仅仅是通过IIFE方法不污染全局作用域而已;
- 不同页面的JS中有很多的冗余代码;
要达到的目标:
- 能够通过引入模块化的方法,解决代码的冗余,提高编码的效率;
- 能够支持ES6的特性,方便以后的使用;
- 能否实现Vue单文件那样的组件化?
解决方案:
对于目标1和2, 如果仅仅为了支持模块化的话,可以考虑使用AMD(RequireJS)或者CMD(SeaJS)这两种方案;但,我们还是希望能够使用ES6的新特性,比如箭头函数,promise,async/await等很有用的特性,所以,模块化的机制就采用了es6的import/export。
对于目标3,Vue单文件那样的组件化,本质上是将一个组件对应的js/css/html放在一起,这样可以很方便的进行处理和复用;宏观上讲,每个页面都可以通过一个个组件搭建起来,这种复用的方式,很简洁很强大。直接使用Vue的重构成本太高,所以,我们希望能够采用类似的思想实现,这个问题我们后面会有探讨。
使用了es6特性后,源代码就不能够直接在浏览器上运行了,需要经过构建工具的处理,比如fis3或者webpack;这样的话,每次都需要编译,开发的时候会不会不方便呢?这个问题在后面我们会有方法来解决。
尝试fis3
现有工程用的构建工具就是fis1.9,不过仅用了混淆代码和压缩的功能;不过我们发现fis3的功能已经足够实现我们的需求了;所以,首先尝试的就是fis3.
fis3的原理
fis3的构建过程可以概括为三步:
- 扫描工程目录拿到文件,并初始化为一个文件对象列表;
- 首先,对每个文件进行单文件编译(编译的时候会读取用户对该类文件设置的文件属性以及插件进行编译);
- 然后,获取用户设置的package插件,进行打包处理;
对文件属性的设置,以及插件的使用,都在fis-conf.js中配置就行;
fis3其实天生就非常适合多页面工程的构建,因为fis3是不会修改你的项目结构,除非你fis3你打包的时候需要合并某些文件;用一句话来解释就是,fis3从所有文件上流过,根据文件对象上的设置对文件进行编译,所有文件编译完后,根据设置的打包配置进行打包。
fis3的进一步的解释可参考1与2;
工程目录结构
.
├── 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
fis3方案的解释
配置文件fis-conf.js
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的构建原理可大概分为五步:
- 读取配置,初始化Compiler对象,同时加载所有的plugins,调用Compiler的run方法;
- 从entry出发,调用配置的模块loader进行编译,再对模块依赖的模块进行编译;
- 完成编译后,得到各个模块间的依赖关系;
- 根据entry和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入输出列表;(这是修改输出内容的最后机会)
- 确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写到文件系统中。
有几个概念需要解释下:
- loader,webpack把一切文件都当做模块,而模块的编译就需要对应的loader;
- plugin,webpack会在特点的时间点广播特定事件,plugin可以监听特定的事件后执行定制的逻辑,然后调用webpack提供的api改变webpack运行的结果;
- 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;复制代码
基本思路:
- 对于不需要webpack处理的文件,比如libs/下的,使用CopyWebpackPlugin直接输出;
- 对于有依赖的模块,则需要使用webpack处理;由于webpack是从entry开始解析以及相应的依赖,然后将之一起打包为对应的文件;所以,entry一般是js文件(因为js文件内可以使用import/export来使用依赖模块);那js对应的html怎么办呢?可以通过HtmlWebpackPlugin将对应的js文件插入到html中。
webpack处理的时候会遇到各种路径问题,可参考10;
三个目标的实现:
1. webpack是怎么支持模块化的?
webpack也是按照commonjs的规范来处理模块。
与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方案。
参考
3. fis3 对npm的node_modules模块的支持;