前言
前端的技术发展真的快,好了,废话不多说,先说说使用browserify及gulp配合构建的例子以及个人认为的缺陷。
后面会将这个项目的构建工具以及demo源代码发出来的。。。但是但是但是,browserify+gulp还是没能解决开发和在客户端部分加载的问题,也没办法打包成为多个文件,然后像seajs那样模块加载—-这个可是很致命的。browserify的出现至少展示了前端工程化的新方向—-预编译,但是还差一点,而我就是要解决相差的那一点甚至为了解决这个问题而要推倒重来—甚至不用browserify。
不过说起来,这个demo和构建工具虽然是半成品,但也可以给大家一些启示,起码我自己在做这个预编译样式和代码的时候逐渐明白了方向。
简介
构建的系统大约分成几部分–针对每一个项目的配置文件,build.js,构建工具以及构建的产物和资源列表。
大约如下:
还有,一个用来演示的构建网站,php+composer的,而mvc的只是个人根据网上的改改而成。
我们还是先来看看演示网站的实际成品吧,看看构建出来的结果产物:
静态资源的构建规则
1、对于成熟的外部类库,譬如,jquery,mement这些,可以指定为静态资源,然后,构建工具会自动计算出这些资源的md5码,然后就会在页面上面构建引用url。
来看看jquery的引用过程:
a、在配置文件build。js这样设定:
b、工具构建后得到的对应的资源表:
c、好了,在页面如何引用:
页面的引用是根据配置文件规定的资源id;来的,事实上,前端构建以后,所有资源都应该用文件列出来,而资源路径都应该要自动构建,所有资源都带有md5甚至样式和脚本和图片名字都包含md5的一个重要考量是,构建出来的文件可以作为本地缓存,以后改改路径就可以了,彻底利用缓存。
而脚本路径引用逻辑是这样的:
<?php
/**
* Created by PhpStorm.
* User: hasee
* Date: 2016/10/5
* Time: 13:49
*/
class UrlHelper{
private static $res_json;
private static $res_setting_json;
public function __construct()
{
if(!isset(self::$res_json)){
$_content=file_get_contents(BASE_PATH."/data/resource.json");
self::$res_json = json_decode($_content, true);
$_content=file_get_contents(BASE_PATH."/data/res.setting.json");
self::$res_setting_json = json_decode($_content, true);
}
}
public function CssUrl($resID){
$res_mode="dev";
if (self::$res_setting_json["mode"]=="dev"){
$res_mode="dev";
}
else{
$res_mode="dist";
}
if(self::$res_json["styles"]&&self::$res_json["styles"][$resID]){
$res_rule=self::$res_json["styles"][$resID];
return CONTEXTPATH.$res_rule[$res_mode];
}
else{
return '<!--css resource named '.$resID.' is not found or mode '.$res_mode.' is not found-->';
}
}
public function JsUrl($resID){
$res_mode="dev";
if (self::$res_setting_json["mode"]=="dev"){
$res_mode="dev";
}
else{
$res_mode="dist";
}
if(self::$res_json["scripts"]&&self::$res_json["scripts"][$resID]){
$res_rule=self::$res_json["scripts"][$resID];
return CONTEXTPATH.$res_rule[$res_mode];
}
else{
return '<!--js resource named '.$resID.' is not found or mode '.$res_mode.' is not found-->';
}
}
public function StaticCssUrl($resID){
$res_mode="dev";
if (self::$res_setting_json["mode"]=="dev"){
$res_mode="dev";
}
else{
$res_mode="dist";
}
if(self::$res_json["static"]["styles"]&&self::$res_json["static"]["styles"][$resID]){
$res_rule=self::$res_json["static"]["styles"][$resID];
return CONTEXTPATH.$res_rule[$res_mode]."?v=".$res_rule["md5"];
}
else{
return '<!--css resource named '.$resID.' is not found or mode '.$res_mode.' is not found-->';
}
}
public function StaticJsUrl($resID){
$res_mode="dev";
if (self::$res_setting_json["mode"]=="dev"){
$res_mode="dev";
}
else{
$res_mode="dist";
}
if(self::$res_json["static"]["scripts"]&&self::$res_json["static"]["scripts"][$resID]){
$res_rule=self::$res_json["static"]["scripts"][$resID];
return CONTEXTPATH.$res_rule[$res_mode]."?v=".$res_rule["md5"];
}
else{
return '<!--js resource named '.$resID.' is not found or mode '.$res_mode.' is not found-->';
}
}
public function CssForModule($module,$resID){
$res_mode="dev";
if (self::$res_setting_json["mode"]=="dev"){
$res_mode="dev";
}
else{
$res_mode="dist";
}
if(self::$res_json["modules"][$module]&&self::$res_json["modules"][$module]["styles"]&&self::$res_json["modules"][$module]["styles"][$resID]){
$res_rule=self::$res_json["modules"][$module]["styles"][$resID];
return CONTEXTPATH.$res_rule[$res_mode];
}
else{
return '<!--css resource named '.$resID.' is not found or mode '.$res_mode.' is not found-->';
}
}
public function JsForModule($module,$resID){
$res_mode="dev";
if (self::$res_setting_json["mode"]=="dev"){
$res_mode="dev";
}
else{
$res_mode="dist";
}
if(self::$res_json["modules"][$module]&&self::$res_json["modules"][$module]["scripts"]&&self::$res_json["modules"][$module]["scripts"][$resID]){
$res_rule=self::$res_json["modules"][$module]["scripts"][$resID];
return CONTEXTPATH.$res_rule[$res_mode];
}
else{
return '<!--js resource named '.$resID.' is not found or mode '.$res_mode.' is not found-->';
}
}
public function ActionUrlFor($model,$actionName){
$_php_name="/admin.php";
if($model=="admin"){}
else{
$_php_name="/index.php";
}
return CONTEXTPATH.$_php_name."/".$actionName;
}
}
看不懂不要紧,因为这里只是着重说一下相关流程,等下会有demo放出去慢慢尝试的。
d、页面上面的渲染结果:
样式和图片的构建规则
样式和图片的构建,样式支持scss,支持import,同时可以设定多个样式合并在一起,还有就是,构建样式的时候会将图片自动放到产出目录,给图片加上md5特征码,顺便将样式里面对应图片的路径改成产出图片路径。
现在我们来看看一个例子:
a、我们现在有两个样式,叫global.scss,reset.scss需要合并在一起,还有一个样式叫login.scss的,需要构建一下,用于登录页面。
登录的样式:
我们需要这样设定:
构建以后变成了:
资源文件对应部分如下:
好吧。。我将全部内容都贴出来算了,等下再解释:
{
"styles": {
"all": {
"dev": "/static/dist/css/all.2b7f3e80f486e8e74e1b7c06fa0f283f.css",
"dist": "/static/dist/css/all.2b7f3e80f486e8e74e1b7c06fa0f283f.min.css",
"md5": "2b7f3e80f486e8e74e1b7c06fa0f283f"
}
},
"scripts": {
"index": {
"dev": "/static/dist/js/index.4bec43ad342ba77e520e070fdab275b5.js",
"dist": "/static/dist/js/index.4bec43ad342ba77e520e070fdab275b5.min.js",
"md5": "4bec43ad342ba77e520e070fdab275b5"
},
"base": {
"dev": "/static/dist/js/base.f4adca34785cd216d7a53aba2f69b437.js",
"dist": "/static/dist/js/base.f4adca34785cd216d7a53aba2f69b437.min.js",
"md5": "f4adca34785cd216d7a53aba2f69b437"
}
},
"static": {
"styles": {},
"scripts": {
"jquery": {
"dev": "/static/lib/jquery-1.8.0.js",
"dist": "/static/lib/jquery-1.8.0.min.js",
"md5": "04903dc2c64729e82e640d527223c9af"
},
"seajs": {
"dev": "/static/lib/sea-debug.js",
"dist": "/static/lib/sea.js",
"md5": "1c05fca93443a604bb4455873366ad21"
},
"ResourceHelper": {
"dev": "/static/helpers/ResourceHelper.js",
"dist": "/static/helpers/ResourceHelper.js",
"md5": "36ee7f820fdc2b043bedc6dbb2ce3f53"
},
"moment": {
"dev": "/static/lib/moment.js",
"dist": "/static/lib/moment.min.js",
"md5": "0dd576e025858b47248c33dff8190711"
},
"react": {
"dev": "/static/lib/react.js",
"dist": "/static/lib/react.min.js",
"md5": "b941048c2c0025f56c5ecb0e4ecdc2c5"
},
"react-dom": {
"dev": "/static/lib/react-dom.js",
"dist": "/static/lib/react-dom.min.js",
"md5": "ee7a0372099ba328275eedc45c8d34b6"
},
"ejs": {
"dev": "/static/lib/ejs_production.js",
"dist": "/static/lib/ejs_production.js",
"md5": "bf9e301590ff13ac104b924e54632f7c"
},
"laytpl": {
"dev": "/static/lib/laytpl.js",
"dist": "/static/lib/laytpl.js",
"md5": "ae95df82da180a6cca9911eba78eb635"
}
}
},
"modules": {
"admin": {
"styles": {
"login": {
"dev": "/static/dist/css/login.a1554d99bc9bae2ec8e7b03bc6127388.css",
"dist": "/static/dist/css/login.a1554d99bc9bae2ec8e7b03bc6127388.min.css",
"md5": "a1554d99bc9bae2ec8e7b03bc6127388" }
},
"scripts": {
"login": {
"dev": "/static/dist/js/login.cdbc1ea9b42d494cf1a9ebd5fad975b1.js",
"dist": "/static/dist/js/login.cdbc1ea9b42d494cf1a9ebd5fad975b1.min.js",
"md5": "cdbc1ea9b42d494cf1a9ebd5fad975b1" }
}
}
}
}
可以看到所有拥有资源id的资源都包括在内。
然后引用的页面内容如下:
<link href="<?php echo CssUrl('all')?>" rel="stylesheet" type="text/css" />
<link href="<?php echo CssForModule("admin","login")?>" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="<?php echo StaticJsUrl('jquery'); ?>"></script>
<script type="text/javascript" src="<?php echo StaticJsUrl('moment'); ?>"></script>
<script type="text/javascript" src="<?php echo StaticJsUrl('react'); ?>"></script>
<script type="text/javascript" src="<?php echo StaticJsUrl('react-dom'); ?>"></script>
<script type="text/javascript" src="<?php echo StaticJsUrl('ResourceHelper'); ?>"></script>
<script type="text/javascript" src="<?php echo StaticJsUrl('laytpl'); ?>"></script>
<script type="text/javascript" src="<?php echo StaticJsUrl('seajs'); ?>"></script>
实际渲染效果如下:
<link href="/eFace/static/dist/css/all.2b7f3e80f486e8e74e1b7c06fa0f283f.css" rel="stylesheet" type="text/css" />
<link href="/eFace/static/dist/css/login.a1554d99bc9bae2ec8e7b03bc6127388.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="/eFace/static/lib/jquery-1.8.0.js?v=04903dc2c64729e82e640d527223c9af"></script>
<script type="text/javascript" src="/eFace/static/lib/moment.js?v=0dd576e025858b47248c33dff8190711"></script>
<script type="text/javascript" src="/eFace/static/lib/react.js?v=b941048c2c0025f56c5ecb0e4ecdc2c5"></script>
<script type="text/javascript" src="/eFace/static/lib/react-dom.js?v=ee7a0372099ba328275eedc45c8d34b6"></script>
<script type="text/javascript" src="/eFace/static/helpers/ResourceHelper.js?v=36ee7f820fdc2b043bedc6dbb2ce3f53"></script>
<script type="text/javascript" src="/eFace/static/lib/laytpl.js?v=ae95df82da180a6cca9911eba78eb635"></script>
<script type="text/javascript" src="/eFace/static/lib/sea-debug.js?v=1c05fca93443a604bb4455873366ad21"></script>
脚本的构建规则
好了,看了前面两个,实际上脚本的构建也差不多。
但缺点在于,browserify生成的脚本实际上只是放到同一个文件夹里面,然后自己定义了一个require,将模块都放在内部的数据里面,没有办法动态加载任何一个这样的脚本然后再加载里面的任意一个模块。
请看看源代码:
(function outer (modules, cache, entry) {
// Save the require from previous bundle to this closure if any
var previousRequire = typeof require == "function" && require;
function newRequire(name, jumped){
if(!cache[name]) {
if(!modules[name]) { // if we cannot find the module within our internal map or // cache jump to the current global require ie. the last bundle // that was added to the page. var currentRequire = typeof require == "function" && require; if (!jumped && currentRequire) { return currentRequire(name, true); } // If there are other bundles on this page the require from the // previous one is saved to 'previousRequire'. Repeat this as // many times as there are bundles until the module is found or // we exhaust the require chain. if (previousRequire) { return previousRequire(name, true); } var err = new Error('Cannot find module \'' + name + '\''); err.code = 'MODULE_NOT_FOUND'; throw err; }
var m = cache[name] = {exports:{}};
modules[name][0].call( m.exports, function(x) { var id = modules[name][1][x]; return newRequire(id ? id : x); }, m, m.exports, outer, modules, cache, entry);
}
return cache[name].exports;
}
for(var i=0;i<entry.length;i++)
{
newRequire(entry[i]);
}
// Override the current require with this new one
return newRequire;
})({1:[function(require,module,exports){ /** * Created by hasee on 2016/10/5. */ var WebUI = { showConfirm: function (opts) { console.log("confirm pls."); } }; module.exports = WebUI; },{}],2:[function(require,module,exports){ var Child = React.createClass({ displayName: "Child", render: function () { return React.createElement("div", null, " The Child "); } }); module.exports = Child; },{}],3:[function(require,module,exports){ /** * Created by hasee on 2016/10/5. */ //--前端日志记录,以便性能优化。 var Logger = { log: function (message) { console.log("run Logger.log"); } }; module.exports = Logger; },{}],4:[function(require,module,exports){ var Child = require('./Child.jsx'); var Parent = React.createClass({ displayName: "Parent", render: function () { return React.createElement("div", null, React.createElement("div", null, " Hello World "), React.createElement(Child, null)); } }); module.exports = Parent; },{"./Child.jsx":2}],5:[function(require,module,exports){ /** * Created by hasee on 2016/10/5. */ var Logger = require('./assets/logger'); var WebUI = require('./assets/WebUI.js'); var Parent = require('./assets/Parent.jsx'); var NumberSelector = ""; var app = { init: function () { Logger.log("hello"); setTimeout(function () { WebUI.showConfirm({}); }, 1000); ReactDOM.render(React.createElement(Parent, null), document.getElementById('app')); //--模板渲染。 this.renderList(); var t2 = NumberSelector; }, renderList: function () { var tplStr = ResourceHelper.getTemplateFromBaseDir("<h3>{{ d.title }}</h3> <p class=\"intro\">{{ d.intro }}</p> <ul> {{# for(var i = 0, len = d.list.length; i < len; i++){ }} <li> <span>{{ d.list[i].name }}</span> <span>所在城市:{{ d.list[i].city }}</span> </li> {{# } }} </ul>"); //第三步:渲染模版 var data = { title: '前端攻城师', list: [{ name: '贤心', city: '杭州' }, { name: '谢亮', city: '北京' }, { name: '浅浅', city: '杭州' }, { name: 'Dem', city: '北京' }] }; $("#log").append(laytpl(tplStr).render(data)); } }; $(function () { app.init(); }); },{"./assets/Parent.jsx":4,"./assets/WebUI.js":1,"./assets/logger":3}]},{},[5]);
话说
./assets/Parent.jsx
这种形式的require真的丑到爆,当然,browserify可以用将npm install了的模块也打包进去文件里面。。
但是我觉得这个真的是扯谈。。。服务端的代码跟客户端浏览器本来就不一样的,硬要两者合并到时候死都不知道怎么死。
还有一点就是,不支持模块分割。。。有些模块是基本不变的,譬如,我们自己写的ui插件,有些是经常变化的,譬如,业务逻辑。我们需要一个机制可以有模块分割,浏览器加载。seajs当时也做得挺好的。
难道就没有一种像java那样,在文件里面写个package xxx,然后在其他文件上面导入命名空间,譬如,import(‘xxxx’)这样就可以用的机制吗?当然有,下一篇文章就是完成这个的。但是不知道什么时候会写。
资源下载及相关
需要注意的是,构建工具需要npm install一下,然后设定文件里面:
configFile需要换成你自己的项目目录。
下载地址:
这里写链接内容
b、php项目用的是composer,请自行搭建环境然后安装类库。
用来这么多类库的,还要搭建apache服务器,你懂得。
下载地址:
这里写链接内容