# 主题模板样式快速编写框架

基于 FIS3 + LESS 开发环境。

项目仓库地址： [https://git.oschina.net/icode2017/fis3-less](https://git.oschina.net/icode2017/fis3-less) 

以上仓库为私有项目，需项目成员才可以正常访问。 请到 https://git.oschina.net 注册用户，然后将注册的用户名发送到 168189878@qq.com


<img src="https://git.oschina.net/icode2017/fis3-less/raw/master/docs/体验主题模板开发框架的威力.gif" style="width:100%;" />



参考:
* [FIS3](http://fis.baidu.com/fis3/docs/beginning/intro.html)
* [LESS](http://less.bootcss.com/)
* [http-server](https://github.com/indexzero/http-server)

运行开发环境及工具
* Node v4.4.7  [下载](https://nodejs.org/zh-cn/download/)
* npm v2.15.8
★★★★★ **_强烈建议：在安装完 node 后，将 npm 仓库地址修改为 [淘宝 NPM 镜像](http://npm.taobao.org/)_** ★★★★★ 
* fis3 v3.4.24 (cnpm install -g fis3)
* lessc v2.7.2 (cnpm istall -g less)
* http-server (cnpm istall -g http-server) 
* [淘宝 NPM 镜像](http://npm.taobao.org/)
* [http://livereload.com/](http://livereload.com/) 实现即时修改自动更新浏览器的插件 [百度云下载](http://pan.baidu.com/s/1dETpkrB)
* [推荐] [WebStorm](https://www.jetbrains.com/webstorm/download/#section=windows-version) [注册破解](http://idea.lanyus.com/) [百度云下载](https://pan.baidu.com/s/1dFA84rZ)
* Git [百度云下载](http://pan.baidu.com/s/1i4GVHYl)
* TortoiseGit Window 环境的客户端 [百度云下载](http://pan.baidu.com/s/1i4GVHYl)


## 目录结构
```
page
 |-- web1
      |-- css
           |-- inc // 用于 index*.less 的 less 片段文件
                |-- index-var.less // 变量声明
                |-- index-height.less // 高度设置
                |-- index-skin.less // 皮肤样式
           |-- index.less // 非通栏的样式文件
           |-- index-fluid.less // 有通栏情况的样式文件
      |-- imgs // 图片资源
           |-- logo.png // logo 图片
           |-- logo.psd // logo 源图
           |-- banner.jpg // 头部背景图片
           |-- banner.psd // 头部背景图片源图
      |-- inc // 存放用于 index-preview.html 使用的预览数据 html 片段
      |-- index.html  // 模板首页
      |-- index-preview.html  // 用于预览的页面
      |-- nav.html // 栏目页
      |-- news.html // 阅读页
      |-- top.html // 公共头部
      |-- footer.html // 公共底部
css
 |-- core-min.css // 前端公共的 css 文件, 已将 layout.css 合并到尾部
js
 |-- core-min.js // 前端公共的JS 文件
 |-- jquery-min.js 
 |-- myfocus-2.0.1.min.js
 |-- mf-pattern // 用于 myfocus 的效果库
      |-- mf-pattern.js
      |-- mf-pattern.css
soft // 用于开发的支撑软件
fis-conf.js // FIS3 配置文件
package.json // node 项目包描述文件
README.md // 项目说明文件
```

## 开发调试
```
git clone https://git.oschina.net/icode2017/fis3-less.git
cnpm install // 安装依赖包 
npm run dev  // 执行打包程序 
npm run server // 启动开发服务器 (http-server) 默认端口为: 3000, 如果需要调整
// 请到 package.json 中找到 "script" : {"server": "http-server -p 3000 ./output -i false "}  进行修改
```

## 打包后的产出目录
```
output
  |-- assets
       |-- css // 原有项目中已存在，无需复制 
       |-- js  // 原有项目中已存在，无需复制
       |-- design
            |-- page
                |-- web1 // 与真实部署环境一致的目录
                    |-- inc // 内嵌的资源文件，打包后此文件夹已无用， 可删除
                    |-- css
                        |-- inc // 内嵌的资源文件，打包后此文件夹已无用， 可删除
                        |-- index.css // 通过 index.less 编译生成的 css 目标文件
                    |-- imgs // 图片资源目录
                    |-- ... // 其他文件
```

## 附: 心得和遇到的坑
### 1. 前端资源路径想变就能变、无需关注路径引用问题

在开发中与实际部署中，面向前端和合成人员最大的问题就是**路径**问题，后端的访问路径说变就能变，那前端文件中的 js、css、图片等资源文件引用路径该如何写呢？
> 使用与服务器环境一致的绝对根路径，这可能是目前唯一行的通的办法。
但这样做势必要求所有的前端和合成人员要清晰的知道后端静态资源的完整路径，且后端的路径是不可变更的。并给我们带来的问题是， 在开发过程中的不便，路径过长，目录嵌套太深等问题随之而来。
**福利来了** 本项目重点要解决的就是这个问题。
在相对目录下开发，通过 fis-conf.js 中 domain 变量的配置自动生成与目标环境一致的文件，自动为 html、 js、css、图片等资源文件加上 domain 路径。任你后端如何变，我前端也能想变就能变。

```html
<!--如：page/web1/index.html-->
<link rel="stylesheet" href="./css/index.less"/>

<!-- fis-conf.js 中的 doamin = "/assets/design/" -->
<!-- 打包生成的结果为：-->
<link rel="stylesheet" href="/assets/design/page/web1/css/index.css"/>
```



### 2. 发布到正式环境要求将 web1 目录名称替换为 #{specialPath} 变量

没想到后端如此多变，刚刚解决了路径问题， 现在又冒出这个问题。 这个问题比路径问题更为棘手。
如果在开发时，用 #{specialPath} 放到 html 资源引用路径中，这会导致页面无法正常浏览。那修改效果时，会显得相当的麻烦。
如果不用 #{specialPath} 替换 web1 路径， 又无法满足后端的需求。真是难两全呀！

有没有办法让我们开发的时候，还是使用相对路径开发，在发布时再**自动** web1 目录名称替换为 #{specialPath} 呢。一定要注意：是自动而不是手动。

是时候该我出场了。

修改 fis-conf.js, 新增以下内容：
```js
fis.media('prod')
    .match('**.html',{
        deploy:[
            // https://github.com/fex-team/fis3-deploy-replace
            fis.plugin('replace',{
                from: /\/assets\/design\/page\/web1\//ig,
                to: function ($0) {
                    return '/assets/design/page/#{specialPath}/';
                }

            }),
            fis.plugin('local-deliver') 
        ]
    })

```

以上的意思是：新增 prod 打包情况，匹配所有的 html 文件，将里面的 /assets/design/page/web1/ 替换为 /assets/design/page/#{specialPath}/ 。 

```
执行： fis3 release prod  -d ./dist
```

检查 dist 目录里的 html 文件，搞定。

### 3. 后端又来要求了，所有 css 中的资源引用必须使用相对路径。

本来就是使用相对路径开发，实现这个小菜一碟。
```
// 启用相对路径插件
// https://github.com/fex-team/fis3-hook-relative
// cnpm i [--save-dev|-g] fis3-hook-relative
fis.hook('relative');
// 让所有 CSS 文件、html 中内嵌的 CSS 、以及 html 元素中使用的 style 中的样式，都使用相对路径。
fis
    .match('/page/web*/css/**.{less,css}', {
        relative: true
    })
    // 匹配命中的 html 文件中内嵌的 css 部分
    .match('*.html:css', {
        relative: true
    })
    // 匹配命中的 html 文件中的内联样式
    .match('*.html:inline-style', {
        relative: true
    });
```

### 4. 又来要求了，发布时以 zip 形式上传到后端管理中。

其实这个问题是不需要自动化的，但为了探究 FIS3 的潜能，还是决定尝试一下，看能否按要求自动生成 zip 包。

```js
fis.media('prod')
    .match('/page/web1/**',{
        deploy: [
            // 使用 fis3-deploy-zip 插件
            fis.plugin('zip',{
                filename: 'web1.zip' // 生成目录 zip 的名称
            }),
            fis.plugin('local-deliver')
        ]
    });
```

但结果是失败的，有两个问题。

问题一： 生成的 zip 包里的内容路径不对，按要求我们是要把 /page/web1/** 里的内容直接压缩到 web1.zip 包里。
```
目标压缩文件 
web1.zip
  |-- index.html
  |-- ... // 直接就是 /page/web1/ 里的内容
  
实际生成的压缩文件
web1.zip
  |-- assets/design/page/web1/
                          |-- index.html
                          |-- ...
```

以上显然不符合要求。

问题二：查看 web1.zip 包里的 index.html ，发现文件里路径中的 web1 并没有替换为 #{specialPath}。

是否就此打住呢，看到近在咫尺的胜利，不能就此气垒。

经分析（此处省略 5000 字），修改 fis3-deploy-zip 插件源码 和 改变打 zip 包源路径，以下为具体代码。

```js
// 打包前先清空 ./dist 目录，如果不清除，第二次的 zip 包里将含有第一次打包的 zip 文件
// 防止文件重复打包
// 如果是 prod 方式打包, 先在打包前先删除 ./dist 目录
if(fis.project.currentMedia() === 'prod'){
    if(fis.util.realpath('./dist')){
        console.log('dist path is exist! start del dist >>>');
        fis.util.del('./dist');
        console.log('dist is del ' + !fis.util.realpath('./dist'))
    }
}

fis.media('prod-zip')
    // 此处一定要注意, 不能使用 /page/web1/** , 如果使用则上一步 html 中的替换则无效了
    // 必须针对已经打包成功后的 /dist 进行匹配
    .match('/dist/assets/design/page/web1/**',{
        deploy: [
            // 此处已经修改了 fis3-deploy-zip 包内容, 默认情况下是不支持将指定路径里文件打包
            // 默认打包是全路径的, 即无论如何匹配, 最终打包的目录都是从 /assets 开始
            // 修改备注: node_modules/fis3-deploy-zip/index.js 31 行左右
            // list.forEach(function(file) {
            //      var filepath = file.getHashRelease().substring(1);
            // }
            // 新增 配置属性 startPath 一定要以 / 开头, 以 / 结尾
            // 通过计算  startPath.length 来动态决定从哪一层文件打包
            //
            fis.plugin('zip',{
                filename: 'web1.zip',
                startPath: '/dist/assets/design/page/web1/'
            }),
            fis.plugin('local-deliver')
        ]
    });
    
```

下面来享受发布主题模板的快感吧。
```bash
// prod 和 prod-zip 为编译环境配置名称
npm run prod-zip  // 静候几秒钟，直接去拿 ./dist-zip/web1.zip 包吧。
// prod-zip 是写入到 package.json 中 script 对象的命令
// 等同于以下命令: 
fis3 release prod -d ./dist && fis3 release prod-zip -d ./dist-zip

package.json:
...
 "scripts": {
    "dev": "fis3 release -d ./output -wL",
    "server": "http-server -p 3000 ./output -i false ",
    "prod-zip": "fis3 release prod -d ./dist && fis3 release prod-zip -d ./dist-zip",
  },
...

```

为什么要使用两个打包配置呢？ 因为 deploy 阶段的所有操作都是异步方式，无法对插件进行串联顺序操作。所以采用多个打包配置来保证其执行的顺序是可控的，从而保证最终的打包结果的正确性。 


### 5. 通过资源嵌入将大文件拆分成功能单一的小文件

index.less 将一个大的文件拆分为多个功能单一的小文件，然后通过 import 导入到主 less 文件

好处自己体会。

```less
// 导入公共变量
@import url('./inc/index-var.less');

// 其他样式

/* 宽度定义 */
//@import url('./inc/index-width.less'); // 无通栏情况
@import url('./inc/index-width-fluid.less'); // 有通栏情况

/* 高度定义 */
@import url('./inc/index-height.less');

/* 皮肤定义 */
@import url('./inc/index-skin.less');


/* 主导航样式 */
@import url('./inc/index-webNav.less');

/* panel 标题样式 */
@import url('./inc/index-panel.less');

/* 焦点图片样式 */
@import url('./inc/index-carousel-mF_shousports.less');

/* 文章列表样式 */
@import url('./inc/index-listtext.less');

/* 多图滚动样式 */
@import url('./inc/index-images.less');

/* 单个图片样式 */
@import url('./inc/index-image.less');

/* footer样式 */
@import url('./inc/index-footer.less');

```

index-preview.html 用于界面预览的页面

因为 index.html 是模板的主文件，里面全是采用配置化的内容，正常浏览器是无法解析的，需要通过设计器脚本解析和后端解析才能正常显示效果。所以新增了 index-preview.html 文件，并加入了模拟数据，便于能在开发阶段和使用阶段预览效果。

```html
...
<div id="wraper">
...
    <!-- 嵌入用于预览页面的公共头部 HTML -->
    <link rel="import" href="./top.html?__inline">
...
        <div class="module multiModule carousel" data-type="carousel" _autoload="1"
             data-conf="{'title':'新闻中心','titleHref':'#','more':'更多+','isTitle':false,'pattern':'mF_sohusports'}"
             data-label="{'labelName':'docImgList','id':#{A1},'num':6,'isChild':false,'width':465,'height':346,'length':20}">
             
             <!-- 嵌入用于预览页面的测试数据 -->
            <link rel="import" href="./inc/inc-carousel.html?__inline">
            
        </div>
        <!--module-->
...
        <div class="module multiModule listtext" data-type="listtext" _autoload="1" data-conf="{'title':'新闻中心',
            'titleHref':'#','more':'更多+','isTitle':true}"  data-label="{'labelName':'docList','id':#{A1},'num':9,
            'length':26,'isChild':false,'isHit':false,'isDate':true,'dateFormat':'MM-dd','target':'_blank',
            'isColumn':false,'where':''}">
            
             <!-- 嵌入用于预览页面的测试数据 -->
            <link rel="import" href="./inc/inc-listtext26-9.html?__inline">
            
        </div>
        <!--module-->
... 
    <!-- 嵌入用于预览页面的公共底部 HTML -->
    <link rel="import" href="./footer.html?__inline">
</div>
...
```


上面的代码中有没有发现：link rel="import" href="./footer.html?__inline" 这样子的代码？

这就是 FIS3 的资源内嵌的用法。相当于我们后端的 include 功能，好处就太多了， 如：对于演示数据，相同类型的我们只需要一个内容文件即可，用到时，我们直接嵌入进来。

这就是我们程序开发中的单一职责原则。

### 6. less 使用中遇到的坑

在实现的应用中，我们经常碰到这样的样式：
```css
#webNav,
#webNav .webNavBar,
#webNav .webNavBar .nav,
#webNav .webNavBar .nav a {
  height: 50px;
  line-height: 50px;
}
```

通过学习 LESS 发现，是可以通过 :extend() 来实现的
```less
#webNav{
    height: 50px;
    line-height: 50px;
    .webNavBar:extend(#webNav){
        .nav:extend(#webNav){
            a:extend(#webNav){}
        }
    }
}
```

是可以生成目标的 CSS ，但坑也给你准备好了。如果 LESS 中又出现 #webNav{}
```less
#webNav{
    background: #f0f0f0
}
```

编译结果却是这个样子地。
```css
#webNav,
#webNav .webNavBar,
#webNav .webNavBar .nav,
#webNav .webNavBar .nav a {
  background: #f0f0f0
}
```

明显和我们定义的想法相关甚远。是什么原因导致的呢？ 我想肯定是 :extend() 扩展命令它扩展了 #webNav，并将 #webNav 作为变量来处理了。那如何解决这个问题呢？

来个“曲线救国”吧。

```less
///* 定义函数 */
.height_lineHeight(@height:36px){
  height: @height;
  line-height: @height;
}

// 定义用于扩展的参考样式
// 为什么要定义这个样式?
// 因为 less 的 :extend(selector) 在嵌套使用时存在复写 selector 的问题
// 为了不影响正常的样式,所以采用使用参考样式扩展的方式解决此问题
// 这个 bug 的修复好辛苦, 花了 6 个小时解决, 给自己点个赞.
.__hl50{
  .height_lineHeight(50px);
}
.__hl150{
  .height_lineHeight(150px);
}

#header:extend(.__hl150){
  .headerBar:extend(.__hl150){
    .logoWraper:extend(.__hl150){
    }
  }
}

#webNav:extend(.__hl50){
  .webNavBar:extend(.__hl50){
    .nav:extend(.__hl50){
      a:extend(.__hl50){}
    }
  }
}
```

突然间，我发现我真是“懒”到家了，连 height 和 line-height 的赋值都定义成一个带默认值的函数了。以后哪个地方要用，调用一下，传个参就可以了。

自动生成的目标 index.css

```css
/* 高度定义 */
.__hl36,
#topArea,
#topArea .topAreaBar {
  height: 36px;
  line-height: 36px;
}
.__hl50,
#webNav,
#webNav .webNavBar,
#webNav .webNavBar .nav,
#webNav .webNavBar .nav a {
  height: 50px;
  line-height: 50px;
}
.__hl150,
#header,
#header .headerBar,
#header .headerBar .logoWraper {
  height: 150px;
  line-height: 150px;
}
```

是不是感觉高大上，很完美。perfect！ 

