Category Archives: Tech

移动设计师该了解的Android分辨率

安卓手机千千万,屏幕尺寸也千千万。作为移动端的设计师,仅仅用PS或者AI画出视觉稿,明显是不够的,还应该提供不同屏幕的适配方案。

移动端适配和Web页面适配有相似之处,一般来说,纵向由于可以上下scroll,不需要特别严格的控制,但是横向一般都是确定的,对于不同的宽度,设计师心中应该很清楚自己的设计稿应该如何呈现。对于Web页面,为了适应不同宽度的展现效果,主要会使用Responsive Design的设计技巧。Responsive Deisgn的原理是,宽度变化的小时,通过减少空隙或者按照%来计算宽度,宽度变化太大时,就换一种layout。

安卓端的适配也类似,一般来说,都属于宽度变化较小的情况。但是现在大屏手机层出不穷,为了较好的大屏体验,有时可能也需要layout上的变化。

1. 这里就是100px,为什么程序员不能画出一样宽度的?

不同屏幕上,px和px是不一样大的!要说完全一样宽,那必须是100 Inch或者100 mm这样的单位,才可能表示不同屏幕上的完全对等宽度。可惜现实是,机器只认px。更加糟糕的是,安卓程序员只能使用dp单位。

2. 什么是高清屏

高清屏仅仅是分辨率高?1280*720?错。一个像素1px的大小越小屏幕越清晰。你可以理解每个像素点为一个矩形,越小越好。这里就有另外一个参数,就是像素密度,每英寸里有多少个像素,比如魅族MX2的像素密度就是343ppi,就是说每英寸有343个px。显然,像素密度越高,一个像素越小,屏幕越清晰。

3. 安卓程序员控制的dp单位和px如何换算?

其实这个不重要,仅仅是一些数学计算。重要的是,你可以认为每个dp在不同的设备上一样大

4. 不同手机的宽度有多少dp?

公式为,宽的像素数 / ( 像素密度 / 160 )
计算一下我接触到手机的宽度:

魅族MX2: 800 / (343 / 160) = 373.18 dp

小米2S: 720 / ( 342 / 160 ) = 336.84 dp

三星5S: 1080 / ( 432 / 160 ) = 400 dp

可以看到,三星的5S要比小米宽了约20%。如果直接用px像素数计算区别,恐怕程序员永远也实现不了你的设计稿。

5. 高度需要考虑嘛?

如果你的设计需要认真的考虑高度,其实是同理的,按照计算dp的方式,计算出手机的宽和高的dp,然后决定在不同长宽比的情况下,layout如何自动适应。

6. 如何告诉程序员尺寸?

最好是能直接说dp数是多少。

在PS中,设计师一般拖一个1280*720的canvas直接就画了,然后跟程序员说,这个是多少多少px。程序员抓了半天头发,想了半天,要么根据宽度,重新算一遍得到dp,要么就完全凭感觉吧。。

在实践中,一些设计师会以320ppi, 1280*720的屏幕参数作为基准,此时,一个2px = 1dp,这时设备宽度为360dp。这个仅仅是大致的估算,对于小米2s和MX2的适配可以差不多近似。

7. 更好的告诉程序员尺寸的方法

我个人认为,设计师无论画布有多大,在最终导出的标注图时,可以让标注图的px宽度值为一个实际手机的dp宽度值。例如,为小米2S的导出一张宽336px的图,为魅族MX2的导出一张宽为373px的图,看看两张图的效果。

不过,这样,导出的两张图中的细节尺寸不一样大呀?这时候,对于固定px宽度的元素,设计师就要以某个宽度为准,标注以后,另外一张图在实现的时候,细节尺寸也按这个来,中间的空隙可以自由伸缩。

程序员看到这样的标注图,也很轻松,因为直接认为1px=1dp了,不用换算,直接写就行。

ClassLoader内存溢出-实例分析

本文地址:http://nius.me/classloader-me…-leak-in-cases/

续上篇:ClassLoader内存溢出-从tomcat的reload说起

上一篇简要分析了内存溢出的原理,并提供了Mattias大神写的防御插件。Mattias大神还写了一篇非常手把手的教程Classloader leaks I – How to find classloader leaks with Eclipse Memory Analyser (MAT)教大家如何追踪ClassLoader内存溢出。

不过Mattias的插件也并不能适用所有的场合。比如我实验室遗留下来的java web项目(%>_<%)。只能自己上了!

原理回顾

回顾一下ClassLoader内存溢出的原理:

  1. web容器,一般会用WebAppClassLoader来加载其中一个app的所有类。
  2. 但是在启动web容器的时候,很多类是在其他ClassLoader(就是WebAppClassLoader被实例化之前)那加载的,比如jre的类,使用的一些库的类。注意,这里的加载,仅仅是加载类对象,例如加载了AppContext这个类,但是并不代表这个类已经有实例化的对象了。仅仅是一个Class对象存在。
  3. 等到WebAppClassLoader加载完需要的类之后,就开始启动web app,实例化servlet、spring等等这些东西,这时候,如果存在一个由其他ClassLoader加载的类,实例化了某一个对象,这个对象又保持了当前的WebAppClassLoader对象的引用,就造成了溢出。
  4. 因为在gc的时候,因为一些原因,例如单例模式,让其他ClassLoader加载的类的对象没有被回收,于是WebAppClassLoader就不会被回收。
  5. 进一步导致,由WebAppClassLoader加载的类对象无法回收,但是这些类实例化的对象可能会被回收。类对象包括这个类的静态成员,所以这些东西都进入了Perm区。

我们要做的,就是把捣蛋的对象找到,对症下药。

1. Proxool没有正确关闭

Proxool是众多数据库中连接池中口碑最好的一个。虽然很多人认为应该把连接池这种杂事交给web容器。在Spring中配置的Proxool一般如下:

<bean id="dataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
        <property name="alias" value="proxoolDataSource" />
        <property name="driver" value="${connection.driver_class}" />
        <property name="driverUrl" value="${connection.url}" />
        <property name="user" value="${connection.username}" />
        <property name="password" value="${connection.password}" />
        <property name="houseKeepingSleepTime" value="${connection.housekeepingsleeptime}" />
        <property name="prototypeCount" value="${connection.prototypecount}" />
        <property name="maximumActiveTime" value="${connection.maximumactivetime}" />
        <property name="statistics" value="${proxool.statistics}" />
        <property name="simultaneousBuildThrottle" value="${proxool.simultaneous.build.throttle}" />
        <property name="maximumConnectionCount" value="${proxool.maximum.connection.count}" />
        <property name="minimumConnectionCount" value="${proxool.minimum.connection.count}" />
    </bean>

 

注意!这个bean没有destroy-method!连接池不会自动关闭!对于实验室的demo型项目,其实我也理解为啥毕业的师兄们都不管这些事。。但是,Proxool的DataSource类没有close方法。囧。中文社区有很多奇葩硬是在bean上加上destroy-method="close",这只能让spring报错,还是没有释放连接池。

于是,大杀器:写一个ServletContextListener

    public void contextDestroyed(ServletContextEvent event) {
        ProxoolFacade.shutdown(1000);
    }

 

然后在web.xml中注册这个listener

 <listener>
        <listener-class>me.nius.mlp.ProxoolShutdownListener</listener-class>
    </listener>

 

嗯。再trace一下,果然对WebAppClassLoader的引用少了Proxool,不过还有一大票其他的=。=

2. jdbc drivers没有取消注册

首先,要使用jdbc的驱动都会到java.sql.DriverManager那注册。当程序reload的时候,应该要deregister。当然tomcat会帮你做这件事,如果你自己没做,在reload的时候,会收到一个warn,说上一次没取消注册,为了防止memory leak,帮你deregister一下。

解决方案同上,写一个ServletContextListener,在contextDestroyed方法里做一下就好了。

3. mysql jdbc driver的AbandonedConnectionCleanupThread

解决了上面的问题,发现居然还有跟jdbc有关的引用。这个好像是mysql专有,不知道oracle或者其他数据库的驱动有没有类似问题。根源在于AbandonedConnectionCleanupThread。这个类看起来是用来清除一些没用的连接的,但是好心办了坏事,在reload的时候,这个对象不会被回收,而且保持了对WebAppClassLoader的引用!

同理,写个方法放到ServletContextListener里

 void shutdownCleanupThread(){
        try {
            AbandonedConnectionCleanupThread.shutdown();
        } catch (InterruptedException e) {
            logger.warn("SEVERE problem cleaning up: " + e.getMessage());
            e.printStackTrace();
        }
    }

 

这个似乎是mysql驱动的一个bug:http://bugs.mysql.com/bug.php?id=69526

4. sun.awt.AppContext

这个是最经典的。很多网上的case study都是讲的这个。这个AppContext是个单例模式,而且是jre的库,所以这个类早早就加载了。但是并没有用到。之后,程序中使用到了这个,调用AppContext.getInstance(),此时这个类保存了当前的ClassLoader的引用,就是WebAppClassLoader!

这个的解决方案跟前面的不同,前面都是关闭的时候把那些对象销毁,这里要在系统加载的时候,提前调用AppContext.getInstance(),而且要用root ClassLoader来干这件事。

     /**
     * Init AppContext using root ClassLoader to prevent memory leak
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        try {
             final ClassLoader active = Thread.currentThread().getContextClassLoader();
             try {
              //Find the root classloader
              ClassLoader root = active;
              while (root.getParent() != null) {
               root = root.getParent();
              }
              //Temporarily make the root class loader the active class loader
              Thread.currentThread().setContextClassLoader(root);
              //Force the AppContext singleton to be created and initialized
              sun.awt.AppContext.getAppContext();
             } finally {
              //restore the class loader
              Thread.currentThread().setContextClassLoader(active);   
             }
            } catch ( Throwable t) {
             //Carry on if we get an error
            }
    }

 

注意,这里并不是实现ServletContextListener的contextDestroyed方法,而是contextInitialized方法。

Maven jetty plugin 9.1.0的bug

解决了上面的问题,发现用jetty跑,还是没法销毁,而保存WebAppClassLoader的对象,正式Maven jetty plugin里面的对象!这时用tomcat跑已经正常了,找不到对其的引用了,虽然还在内存里(因为java 7回收比较懒)。

仔细看了一下,jetty插件注册了一个ApplicationShutdownHook,这玩意是用在jvm退出的时候干一些释放资源的活用的。这个Hook里有一个org.eclipse.jetty.util.thread.ShutdownThread,这玩意保存了所有的WebAppClassLoader的引用!这不是搞笑么!居然被我发现了一个bug!

更新了一下插件版本到9.2.0,唔,fix了,确实是bug。头一次领悟到一个google不到的bug,想想还有点小激动呢。

这样一来,实验室的项目总算服服帖帖的能够正常reload了,再也不用一次次重启tomcat了,内牛满面啊。

Tomcat/Jetty Memory Leak Prevention

ClassLoader内存溢出是一个古老的话题,厂商怎么可能不重视!诺:

tomcat MemoryLeakProtection
Jetty Howto Prevent Memory Leaks

web容器早就备好了各种武器供大家使用啦!实际上大家可能用了都没意识到,因为避免了,所以不会溢出了= =!

不过,还是会出现遇到容器没法解决的情况,这时候,就得自己上手了!就我个人的使用体验来看,MATVisualVM好用,再次推荐Mattias的手把手教程:Classloader leaks I – How to find classloader leaks with Eclipse Memory Analyser (MAT)

手工实现Js的Promise API

原文地址:http://nius.me/implementing-js-promise-api/

用node开发一阵子的同学,很容易就陷入了callback hell,一层层的嵌套回调,代码实在没法看。于是很多异步转同步工具库出现了。Javascript的harmony版本(ES 6)提供了异步转同步的标准工具:Promise。相最近仔细研读了html5rocks的这篇文章Javascript Promises。里面有很多Promise的例子。不过行文有点快,得慢慢理解。这里为很多还不是很明白Promise的魔法到底是怎样实现的同学,提供一个一步步设计、实现Promise API的讨论。

普通的回调

异步代码都是通过回调函数来实现的。一段ajax请求的代码可能如下:

var req = new XMLHttpRequest();
req.open(url);
req.onload = function(data){ /* callback */ };

//in jQuery
$.get(url, function(data){ /* callback */ });

出现一层层的回调的原因,就在与,在外围的回调函数中,会继续编写异步代码,绑定新的回调函数。例如:

req.onload =       //回调函数绑定
  function(data){      //回调函数的方法头
    var next = parseData(data);   //回调函数同步方法体
  
    anotherReq.open(next);        //回调函数异步方法体
    anotherReq.onload = function(nextData){  
         //... maybe more
    }
}

实现目标:then链

明确一下我们的目的,就不需要编写层层嵌套的回调函数。实现成readFileSync这种的API是不合适的,因为这种实现一定会等待异步代码返回结果,大大降低性能。我们要让代码看起来是同步的,所以,应该是这样的方式:

doSomething()
.then(doAnotherThing)
.then(doThirdThing);

即通过某种Executor,帮助我们实现自动嵌套绑定回调,让代码看起来是先后的,执行起来是异步的。

1. 代码解耦合

从上面我们看出,一段回调代码会包括三个部分

  1. 回调函数绑定
  2. 定义回调函数方法头
  3. 定义回调函数同步方法体
  4. 定义回调函数异步方法体,就是里面嵌套的深层次回调

我们要把函数绑定和函数定义分离解耦,这样才能把嵌套代码拆出来。

//basic version
var callback = function(){}
req.onload = callback;

//more flexible version
var callback = function(){}

var bind = funtion(callback){
  req.onload = callback;
};

bind(callback);

basic version是很常用的分离方法,但是这种方式很难想象出下一步我们该怎么做。变成more flexible version,大家一看就懂了。绑定过程也变成了定义的过程,延迟bind函数的调用,就可以在bind函数上做文章。上Promise!

//ATTETION: Pseudo code
var callback = function(){};

var promise = new Promise(function(callback){  //bind函数体
  req.open(); //async
  req.onload = callback;
})
promise.then(callback);    //调用bind函数

这段代码是不是很神奇了!(虽然还是看不出来怎么实现then.then.then)但是至少我们分离了回调函数绑定和定义,并延迟了绑定的发生。把一切都交给了promise对象,让promise对象来决定中间的过程怎么做!

2. 实现then链

我们要解决的问题是嵌套回调函数。就是说,callback内部也会有回调函数。怎么办?当然是,把内部的回调绑定和回调函数定义也分离!

//ATTETION: Pseudo code
var callback, innerCallback;

var promise = new Promise(function(callback){
  req.open(); //async
  req.onload = callback;
})

callback = function(){

   var innerPromise = new Promise(function(innerCallback){
     innerReq.open();  //async
     innerReq.onload = innerCallback;
   })
   innerPromise.then(innerCallback);
}

promise.then(callback);

是不是有点像了!我们只要把innerCallback传给内部的innerPromise的then函数就行了!而innerCallback完全可以在其他地方定义!

我们的目标是then链,但是现在,有两个Promise对象,第二个promise在回调函数执行的时候才被定义,和外面的promise不在一个次元啊。也就是说,要实现then链的话,回调函数们都被传给最外围的promise,而内层的innerPromise的then函数都没办法触发。

那就让他们到一个次元去!用callback的返回值把innerPromise传回去,别忘了,callback可是被传给了promise对象呀,所以promise完全可以拿到callback的返回值呀!这样,外围promise就有了内部promise的引用,就可以把第二个、第三个then函数接到的参数,依次告诉内层的promise!

//ATTETION: Pseudo code
var callback, innerCallback;

var promise = new Promise(function(callback){
  req.open(); //async
  req.onload = callback;
})

callback = function(){

   var innerPromise = new Promise(function(innerCallback){
     innerReq.open();  //async
     innerReq.onload = innerCallback;
   })
   return innerPromise;  //告诉外部的promise
}

promise.then(callback).then(innerCallback);

3. 魔法所在:then方法

是不是还是有点绕?这里magic的地方在于,promise对传入的callback会进行改造,再传给bind函数,为了更明确一点,我替换一下变量名:

//ATTETION: Pseudo code
var callback, innerCallback;

var promise = new Promise(function(resolve){  //注意,这里的resolve已经不是callback了!
  req.open(); //async
  req.onload = resolve;
})

callback = function(){

   var innerPromise = new Promise(function(innerResolve){ //这里也不是innerCallback了!
     innerReq.open();  //async
     innerReq.onload = innerResolve;
   })
   return innerPromise;  //告诉外部的promise
}

promise.then(callback).then(innerCallback);

是不是有点明白了?then方法对callback进行改造,例如,把callback外包一个resolve函数。来,自己动手实现一个naive版的Promise!

//ATTETION: Pseudo code, but you can run it.
Promise = function(bind){
  this.level = 0;  //
  this.current = 0;
  this.callbacks = [];
  
  this.bind = bind; //保存传入的bind函数
};

Promise.prototype.then = function(callback){

   var self = this;
   
   this.callbacks[this.current++] = callback;
   
   if(!self.isSolved){
     //从callbacks队列的头部取下第一个resolve,进行绑定
     var res = self.getResolve(this.nextCallback());
     self.bind(res);
     self.isSolved = true;
   }

   return self;   //返回自己,形成then链
};

/**
 * 将callback转换成resolve
 * 
 * @param  {Function} callback 
 * @return {Function} resolve           
 */
Promise.prototype.getResolve = function(callback){
    if(!callback) return;
    var self = this;

    return function(){
      console.log('resolve');

      var nextPromise = callback();   //获得返回的promise,注意callback调用没有参数
      if(nextPromise instanceof Promise){
      
        nextPromise.inherit(self);   //传递当前状态给nextPromise
        nextPromise.execute();   //执行下一个promise
      }
   };
};

Promise.prototype.nextCallback = function(){
   return this.callbacks[this.level++];
};

Promise.prototype.inherit = function(promise){
   this.level = promise.level;
   this.current = promise.current;
   this.callbacks = promise.callbacks;
   this.isSolved = promise.isSolved;
};

/**
 * 执行一个promise,从callbacks中获得一个callback,进行绑定
 */
Promise.prototype.execute = function(){
    var res = this.getResolve(this.nextCallback());
    var nextPromise = this.bind(res);
    if(!nextPromise) return;
    nextPromise.inherit(this);
    nextPromise.execute();
};

这里通过一个数组来记录所有通过链式调用传入的callback方法,然后通过inherit方法,把所有状态完完全全的交给nextPromise,从而让nextPromise继续绑定剩余的callback方法。

这个假想实现有一处硬伤,就是仅仅能让异步代码依次运行,但是异步代码之间没有通信!就是innerPromise得不到外围promise执行结束后的数据呀!es6的解决方案是,如果callback方法返回的不是一个promise,而是一个值,那么这个值将作为下一个callback的首个参数(唯一参数)。

真实的实现还要复杂很多,参考polyfill,这是一个能用现有js实现的Promise库。这个库要做到异步函数立即执行。上面的假实现仅仅做到在调用then时执行bind,实际上bind希望马上执行,毕竟有异步代码在里面,修改上面的代码,这个也是可以实现的。

总结

是不是已经想明白啦?Promise实际上充当了一个执行者的角色,动态的执行每一层次的异步函数的绑定,从而让代码可以看上去,一步步来,then and then and then…真是人类智慧的结晶啊~

数据库事务

学数据库的时候就会经常提到事务这个词,事务的ACID,事务的rollback,为什么需要事务。但以前写的项目中,很少重视这个问题(好像因为我很少写后台的缘故)。最近写高并发的东西需要事务体现的特别明显。 最简单的例子:

public void updateName(Long id, String name){
    User user = userDao.findById(id);
    user.setName(name);
    userDao.save(user);
}

public void updateGender(Long id, boolean male){
    User user = userDao.findById(id);
    user.setGender(male);
    userDao.save(user);
}

如果直接在方法中这么写,会出现并发问题。由于多个线程最终还是串行的(CPU是串行的),所以,这段代码的执行顺序可能如下:

thread1: User user = userDao.findById(id);  //   name: "Alis", gender: false
thread2: User user = userDao.findById(id);  //   name: "Alis", gender: false

thread1: user.setName("Bob"); // name: "Bob", gender: false
thread2: user.setGender(true); // name: "Alis", gender: true

thread1: userDao.save(user);   // name: "Bob", gender: false
thread2: userDao.save(user);   // name: "Alis", gender: true

//expected result in database: name: "Bob", gender: true;
//final result in database: name: "Alis", gender: true

可以看出,我们实际上想最终得到一个男Bob,结果得到了一个女Alis。当然,由于并发执行,并不能保证执行顺序,也有可能是一个女Bob,男Bob,完全凭运气。因为两个线程不曾互相通气,说哪个属性已经被改变了。

这个场景似乎在Web上很不容易想象出来,但仔细一想又会存在。例如,小明正在修改自己的个人信息,同时,由于连续使用3天,系统给小明的信息中更新了一下积分,如果积分和个人信息都放在profile表里,就很有可能发生并发,导致数据覆盖。虽然可能性很小,但一旦发生,结果往往是不可挽回的。

解决的办法很简单,事务。

JDBC就提供了事务

conn = DriverManager.getConnection();
conn.setAutoCommit(false);

//do queries...

conn.commit();

使用Hibernate,也提供了相应的接口

Transaction tx = session.beginTransaction();

// do queries...

tx.commit()

使用Spring的话,也提供了@Transactional注解,在需要事务的方法上加上注解就好了

@Transactional
public void updateName(){ 
     //...
}

想来Spring还着实做了不少简化开发的工作啊。

状态模式、面向对象与函数式

最近好友墙裂向我推荐Clojure,就仔细看了一下这门Lisp方言。《黑客与画家》中经常提到的一个概念就是Lisp等语言的比其他的编程语言在表达能力上更加强大。我对Clojure仅仅看了两天,不过Javascript倒是用了两年了,Js的设计中借鉴了很多函数式语言的特性,又同时借鉴了一下Java的语法,Java->Javascript->Lisp倒是形成了一个不错的学习曲线。

设计模式是OO编程中必不可少的一部分,但同时,很多人又批评这完全是由于OO语言自身的缺陷导致的。最著名的莫过于Peter Norvig在1996年的一份讲稿: Design Patterns in Dynamic Programming。这里指出23种模式中,有16种模式在Lisp是语言本来就有的特性或者实现起来更加简单的(16 of the 23 patterns in Design Patterns were “invisible or simpler” in Lisp)。先来看一个简单的模式,状态模式。

状态模式

我相信使用Java或者C++的童鞋对状态模式一定不陌生,简单易懂,经常用到。

状态模式

如上图。Context类有多个状态,有一个currentState属性,当调用Context.handle时,委托给currentState.handle()。通过实现State接口,可以定义多种状态,通过切换具体的object,可以实现状态切换。

是否需要这么多State类

状态模式的精髓在于,通过不同的State类来代表不同的状态。等等,真的是类嘛?实际上是State各个实现类实例化之后的对象。什么是一个对象?对象的概念在很多语言中都有,无论在哪,都可以看成是一个数据集合,同时具有多种方法,即对象是有行为的属性集合。在状态模式中,我们实际使用中,仅仅使用了一个object,并没有多次使用这个类(作为object的模板),即没有出现也不需要出现具有不同属性的object。那为什么不直接定义object呢?至少第一眼看上去,可以节省额外的Class开销。

这里我们回顾一下,面向对象编程三大特性:

  1. 封装
  2. 继承
  3. 多态

如果我们直接定义一个个object(或者function),放弃强类型,这三个特性会怎么样?

封装

这个不会有太大的改变,因为不同的object/function仅仅暴露方法就行了,甚至自己就是那个方法,内部运作仍然可以封装。

继承

如果使用object,将不需要一个State接口。接口的意义一方面是多态,即让currentState引用可以指向多个实现类的对象,但对于弱类型语言,这个问题不存在;另一方面是,在编码(其实是编译)阶段防止程序员犯错,State实现类必须实现handle接口,但如果直接定义的是handle function,这个问题也无关紧要。特别是,如果没有完善的测试用例,程序员仍然会犯错,有了完善的测试用例,这点防范措施实在没啥意义。

多态

仍然是多态的,通过切换object/function。

JavaScript实现

大概实现一下。

var context = {
        handle:null,
        init: function(){
            handle = initStateHandle;
        },
        switchState: function(){
            handle = otherStateHandle;
        }
    };

    var initStateHandle = function(){
        //do init state stuff
    }
    var otherStateHandle = function(){
        //do other stuff
    }
    
    //init
    context.init();
    context.handle();
    
    //change state
    context.switchState();
    context.handle();

很久之前我也实现过一个State Pattern in JavaScript,实现思路是类似的。这段代码也可以用Lisp实现,由于刚接触Lisp,就不献丑了。

可以看出,用Js实现的代码短,直接而自然,一看就明白,不需要很多类文件,也不需要费尽心思用继承、委托等等OO技术,仅仅关注好业务逻辑就可以了。

依赖注入

在状态模式中,我们的质疑点是,State的实现类的定义仅仅为了一个对象。Java确实有这么一个框架,帮你实例化一次某个类,然后在全局,任意的注入这个对象,就是Spring。如果把Class定义看做对象定义(仅仅多写几个单词),确实可以应对这个质疑点。但是看看额外的开销吧,要启动一个完整的Spring框架。

当然,依赖注入是一个十分先进的思想,即当我想要任何对象的时候,不需要自己去找,像神一样,说:“要有光”,就把光的对象给你了。十分火的前端框架Angularjs就使用了依赖注入的思想,在函数的参数列表里声明好需要的对象,框架就给你注入了,你完全不用管这个对象是哪来的。

回到Java

喷Java的程序员多了去了,写Ruby、Python、Lisp的程序员,鄙视Java由来已久。但即使这样,为什么Java还这么有市场呢?所有人都会反感Java的Hello world,因为Java一定需要定义一个类,产生了很多额外的代码。纯面向对象的特性,最直接的就是,理解所有的代码的思路是一样的,所有人写出来的代码,也都是一样的。所有的类,都定义在某个package中,有自己的namespace,要解决特定的问题,嗯,一定是找某一个类,并通过某种方式实例化对象。某种意义上,这反而让思维简单了,只需要通过一种单一的方式来理解代码。剩下的,就是用这种单一的方式,该如何处理各种复杂的问题,就出现了设计模式。

所以,Java程序员可以很容易的培训出来,一切都是严谨的,规范的,不可突破的,换句话说,没什么黑客精神的。黑客强调的个性、简单、自由,在这里都没有,带来的好处是,生产力的大大提高,Java程序员遍地都是,从手工艺者,变成了工人。

参考:

  1. Design Patterns in Dynamic Programming
  2. Head First Design Patterns

Mac,Linux,Windows手感对比

本科时候专业课基本用windows和visual studio做的,当时vs的臃肿和必须使用盗版让我对微软毫无好感,然后拥抱了开源世界,用了两年ubuntu linux,直到今年买了Mac。突发奇想做下对比。

常用软件兼容性

Mac:Chrome, Office, QQ, 迅雷,搜狗输入法
Linux: Chrome, 永中Office,Web QQ,浏览器自带下载,fcitx
Windows: 常用软件其实就是指Windows上能用的软件

所以,到今天日常工作上,三个系统基本都能正常使用。体验上Windows最好,Mac次之。

命令行工具

Mac: 自带unix shell,用包管理器homebrew可以安装缺少的linux上常用的工具,也可以装其他shell
Linux: 喜欢命令行的程序员的天堂。用起来十分舒服。
Windows: 用Cygwin可以勉强找到shell的感觉,美中不足是有的时候某些脚本依赖某个shell命令,万一在cygwin中没装,跑步起来还找不着原因

在命令行工具上,三个平台也都做到了可用。体验上linux最好,Mac次之。

软件安装和升级

Windows: 在网上下安装包,然后双击安装。常用软件一般自动升级。不常用软件需要重新安装来升级。
Linux: 用命令行的包管理器搞定一切。一行命令就可以安装chrome,感觉棒极了!升级所有软件只需要一行命令。大部分软件还可以下载源码编译安装,此时升级需要下载新版本源码重新安装。
Mac:图形界面软件,基本下载程序/安装程序安装。命令行软件,通过包管理器,比较弱。还支持从Apple Store安装,但国内大环境下基本没用过。

Linux的一行命令安装/升级已经把体验做到了极致。Mac各种安装方式杂糅。Windows最苦力,啥都要自己下。当然第三方360软件管家什么的可以弥补,但是这些流氓吵来吵去,一不小心qq、搜狗就不能用了。

图形界面

Windows:从win7到win8有个飞跃,以至于让升级系统最快的中国人(因为不要钱)都不升级了。用户最熟悉的,没有办法订制的,看起来凑活的图形界面。但依赖于其的众多图形界面软件(小白请直接理解为常用软件)使其功能最强大。
Linux: 高度可定制化,想怎么改就怎么改的图形界面。但稳定性较差,一不小心碰到G点就坏了。图形界面系统分裂出太多版本,以至于极难制作图形界面软件,以至于整体体验较差。但可以自定义的炫酷拽。
Mac:精致、漂亮的图形界面。用户不能订制,也不想订制,默认的足够好了。基于Mac的图形界面软件的常用软件也渐渐增多。很多开发者工具的图形界面版本都只有Mac的。

Mac作为图形界面的鼻祖,堪称典范。Windows靠兼容的软件功能最强。Linux满足屌丝程序员当神的欲望。

系统稳定性

Windows:蓝屏的,好喝的。不过现在蓝屏情况已经大大减少,但仍然存在。
Linux: 十分稳定,不会挂掉。但是图形界面经常挂。
Mac: 系统挂掉是什么,好吃吗?

三个平台都挺稳定了。

系统安装

Windows: 网络上有出奇多的教程、安装方法,小白用户级别,驱动兼容性最好。
Linux: 发行版的安装都做的十分容易,适合在已有windows的机器上做双系统。驱动不全,有可能装上用不了或者出问题。
Mac:你花上两天时间,看了无数教程,终于在PC上装上了Mac,发现居然跑起来了!接着10秒钟之后CPU因为风扇不转过热挂掉了。

作为程序员

曾经我很排斥windows,觉得开源世界才是程序员的归属。回过头来发现,其实只要在系统上弄好工具链,啥系统都一样。渐渐也没有了偏见。三个系统各有各的长处,各有各的不足,总有用的不爽的地方,总有用的顺手的一天。

从需求到网站:(六)大杀器之CMS

这篇文章欠了好久= =b。前面我们讨论了如何从0到1建立一个网站。这里要讨论的是,从1到1建立一个网站。从1到1,对的,网站已经给你做好了!这才是大杀器!

内容管理系统(CMS)

Content Management System,就是字面意思,管理内容的网站系统。那举个栗子呢?例如新浪网就是一个CMS。新浪编辑们把文章(内容)通过管理员界面添加之后,广大用户就可以看到了,好了,就这么多,就是CMS。是不是很简单,so easy。仔细一想,诶,那所有的网站不都是CMS吗?博客肯定是,博主在管理员界面写好文章,然后大家看。电商也差不多,京东的编辑在管理员界面把商品添加进去,大家在网站上看、购买。

说白了,Web2.0的网站都是CMS,都是管理内容嘛,只是具体内容上有不同嘛。新浪管理新闻,微博管理微博,论坛是管理帖子,博客管理博客,电商管理商品。哇靠,互联网也就这么点东西!内容换个词就是信息,这些其实都是信息系统(Information System),这下知道为啥从前都是叫搞IT(Information Technology)了吧。

突然发现互联网搞来搞去也就是CMS,so sad。

不懂代码的建站

我要做网站,别扯CMS这些没用的!

好了,既然大家都是CMS,Web 2.0都几十年了,这些概念都是陈芝麻烂谷子了,也就是说,前任这些网站的代码都写过了呀,我为毛还要再写一遍呢?为了提升我的逼格嘛?没错,就是不需要写了。

订阅我博客肯定知道我的一篇著名博文《搭建个人博客的通用步骤》。咳咳,说著名是因为你看本博客右边→ →的点击量排名。如果你成功按照这个教程建立了自己的博客,请问你在这过程中写了一行代码吗?你需要懂php吗?你需要懂数据库吗?No, so nice.

WordPress就是一个非常完善的开源免费的CMS系统。前面我们提到过框架的概念,就是把一些轮子给你造好了,减轻程序员的工作量,程序员只用写少量代码就可以完成很多事。而CMS建站系统则把这事干绝了,直接让程序员下班了。只要根据教程,不懂代码的人可以很快建立一个网站,程序员你就失业去吧。

后台与后台与后台

在提到CMS的同时,就不得不提一个令我十分恶心的概念,那就是后台。后台直译英文应该是back end。我们在前文提到的后台就是这个意思,就是网站的服务器端,区别于浏览器端(前台front end)。程序员的世界里,大部分时候,后台都是指服务器端,一般是指后台程序,运行在服务器上的代码。

但是CMS的后台,在中文的神奇语义里,又变成管理员界面(Dashboard/Admin Page)的含义。当你搭建一个wordpress网站的时候,编写博客的界面就是所谓的“后台”。经常有人问我,这个网站要改个xxx能不能改,我说要改后台代码,然后他说后台好像没地方改代码呀?呵呵。然后就没有然后了。

(此段可忽略)不过在程序员的世界里,后端(back end)的概念也会随着语境而变化。当服务器端部署的十分复杂,前面有反向代理服务器,后面有网页服务器、业务逻辑服务器、数据服务器等等的时候,前端(front end)又会变成反向代理服务器,有时又是网页服务器,相应的,业务逻辑、数据处理就会变成后端。

国内著名开源CMS

好了,说了半天,你说不用写代码就建站,但也只说了一个博客网站呀,我要建论坛,搞电商,做团购,咋搞?这些还真都有!

论坛-Discuz!http://www.discuz.net/

老牌论坛建站系统,你只要平时经常逛论坛,基本都是用Discuz做的。后来被腾讯收购了。对是收购,不是腾讯自己做一个山寨的。

电商-ECShop http://www.ecshop.com/

似乎成了创业必备,要么淘宝天猫,要么就自己捣鼓一个这个了。

团购-最土 http://www.zuitu.com/

说实话这是google出来的。当年团购红遍天下的时候,遍地都是开源团购系统,才可能出现全国各地都有大大小小的团购网站。csdn有个集锦:五个免费开源团购建站平台

个性化和建站服务

看起来这些现成的造好的汽车是不是觉得世界一片光明美好!但回头想想这样用别人的CMS直接搭的网站还是不靠谱,我要改个背景颜色,换个LOGO,这咋搞?大的CMS都会有自己的皮肤系统,wordpress这一点就很好,有成千上万的皮肤供你选择嘿~慢慢挑吧。

不行不行,那些皮肤我都看不上眼,我的网站如此之高大上,怎么可以跟别人一样?这时,程序员总算又有饭吃啦!就是改网站界面!可是程序员的审美……唔,所以还是一个设计师搭配一个程序员比较好。于是,一个外包团队就成型啦!一个大专设计师+一个蓝翔CSS程序员,从政府到企业,建站这种事,so easy。

外包团队一般来说仅仅帮你把网站做出来,不负责网站的部署。当然也可以负责部署,这种其实就是换个说法,可以称为建站服务了。去域名注册网站、主机提供商网站上看,都会提供建站服务,一条龙,只要你有钱,做网站不是梦~

程序员的价值

既然开源CMS已经如此牛逼了,为毛互联网公司还要程序员呢?程序员的价值存在于下面几点(个人观点):

大流量

免费的代码虽好,但是任何网站一旦做大,都不可避免存在负载问题。京东之前一做活动就崩溃,后来找来个架构师,重新用Java做了一套,最近才没有出什么问题。如何让网站不轻易宕机?如何保证升级网站的时候用户还可以访问?特殊问题,只能通过程序员的努力来解决。

新业务

卖东西嘛,平时松松返券,打打折,搞个买一送一是很经常的事。可是开源电商系统没有办法为你的特殊活动专门做一些页面、功能。这时候就是程序员的活了。

应对变化

其实上面两条,说白了就是应对变化,需求在变,业务在变,技术在变,时代在变,只有活的程序员才能真正应对这些变化,死的系统做的再好,也会渐渐跟不上时代。所以想认真做互联网创业的人,一开始可以不养技术团队,通过外包等等来搞定,想要真正做大,技术团队是必不可少的。业务牛逼之后,技术也不能掣肘。

从需求到网站——上线啦!

订阅我博客的人应该都看过前面的几篇教程了吧~~~

历时两个多月,中间断断续续开发,统计了一下,自己写的代码大约是1500行。当然包括库以及最终部署所需的代码就不止了~

最终,当当当!

requirement-card.com 上线啦!直接可用哦~

具体用法就不在这里说啦,支持图片、富文本、多人协作,等待你去发现哦~

实在不知道怎么用也可以在网站右下角点帮助啦。

各位晚安~

从需求到网站:(五)前端代码

接前一篇:从需求到网站:(四)技术架构

架构师把网站的架构定下来之后,码农们就开始编码啦!当然,传统的软件工程流程要先做概要设计和详细设计,也就是撰写设计文档。文档是个让程序员又爱又恨的东西。当维护别人代码的时候,巴不得文档越多越好;自己写程序的时候,巴不得不写文档直接编码。必要的设计文档还是很有好处的,特别是当团队成员比较多的时候。

既然是网站,不可避免的需要编写HTML、CSS和Javascript代码,这些代码称为前端代码。

HTML和CSS

这是一对网页好基友,所有的网页都必须通过这两种语言来呈现。HTML负责表达,需要呈现哪些内容。例如:

<a href='www.nius.me‘>Nius' Avalon</a>

上面这段html的A标签(Anchor)就代表一个网页链接,这个链接指向我的博客地址。但是,用户显然不希望看到这样一段代码。于是浏览器就会把这段代码画出来,如果没有对这个A标签定义样式,浏览器会默认给这个A标签一些基本的样式,例如文字颜色是黑色,背景是白色等等。如果想要自己定义样式,就要写CSS代码。

a{
     color: red;
 }

上面这段代码的含义就是,页面上所有A链接,文字的颜色都是红色。如果看到此,对写网页产生了极大的兴趣,欢迎查阅W3C提供的网页编写教程:w3school

那么,需求传奇项目的html和css回是怎样的呢!大家可以查看下面Demo,以及里面的源码。

[iframe http://jsfiddle.net/nius/NV4ET/embedded/result,html,css,js/ 100% 300px]

咦!好像确实跟之前的设计图(传送门)差不多,但是还是不对劲啊!那些{{}}是神马玩意!

这就是传说中的前端模板。估计上一篇文章信息量太大,不好理解,这下大家应该明白模板是啥了。就是填空题嘛,先把要填的空占着。

Javascript

好了,既然html和css,一个掌管显示神马,一个掌管怎么显示,还要js做什么?Js的作用就是,让网页”动起来”,例如本文下方的评论框,点击不同的标签页,显示不同的内容。这个HTML和CSS都不管,只好Js来管了。Js有两个超级牛逼的能力:

在浏览器里操纵html代码

在浏览器里意味着,代码运行在用户的机器上,而不是服务器上。操纵html代码,意味着可以动态的更改显示的内容。同时,html标签里的class和id属性,更改后可以调整html的样式,也就是简介控制CSS。再次,html标签里有一个style属性,可以在里面输入css代码,起到和css同样的效果,即直接控制css。真可谓,挟html以令网页!

向后台发送请求,并接受返回

这个是什么意思呢?看了上一篇文章,大家知道,从url地址打开一个网页,就是向后台发送请求,返回了html文件,以及css文件、js文件等等。当网页打开了之后,不打开新链接,理论上是不会有新的请求的。但是js就可以做到,在当前url地址下,向后台发送请求,并接受返回的值!好吧,这个就是AJAX。

在上一篇文章网站架构中,我提到,后台模板和前端模板两种架构。如果使用后者,那么Js在这里的作用就是要请求后台数据,然后更新当前网页。注意,url地址栏里没有变化哦~(当然js也可以控制url地址栏变化,暂不详述)。

于是,我们的需求传奇的页面就出来啦!

[iframe http://jsfiddle.net/nius/NV4ET/3/embedded/result,html,css,js/ 100% 300px]

前端框架

好了,大家如果有兴趣,可以详细看一下上面Demo的html和Js源码。这里,我使用了一个Js的前端框架:Angularjs。上篇博客发布后,有人问我框架是什么意思,这里解释下。

框架就是说,很多代码,已经有其他的程序员帮助你写好了,你只需要把他的代码拿过来,然后按照他代码里定义的接口和约定,编写少量的代码,就可以完成很复杂的功能。

上面Demo里的Js代码,大家可以看到,只有20多行,这20多行就是按照一定约定来写的,就是说框架说,应该这样写,我就这样写。当然我会在其中添加具体的业务逻辑。同时,html源码里,有np-app,np-model这种不属于html的东东,也是框架要求我这样写的。我按照框架的要求写出代码后,程序就会按照预期来执行。

于是,我就通过20来行的js代码,以及对html的少量改动,就完成了将需求卡片复制10次显示在页面上,每个需求卡片标题不同、优先级(就是颜色)不同的操作。很省事有木有!这就是框架的力量!节省代码!

前端这两年发展迅猛,框架层出不穷,已经选不过来了。不光有Js的框架,还有css的框架。例如上面的css,就是通过compass/scss框架来生成的。

最后,如果你对js也十分感兴趣,仍然推荐W3C的教程:w3school

============ 本文是一个系列,其他已经写好的文章:

从需求到网站:(四)技术架构

接前一篇:从需求到网站:(三)视觉设计

唔,耽误了一阵子,接着写~总算写到程序员可以发挥力量的地方了。不过写的太技术了,就无聊了。尝试写的好玩一点。

上古时代 1.0

上古时代做网站,这要数到上个世纪8、90年代。网站一开始的时候,仅仅是呈现一个文件,这个文件叫html。比如你在你的机器上打开一个word文档,看看写了些啥,那时候的网站就是这样的。只不过,这个html文档不是在你的电脑里,而是在别人的电脑里。例如我放置了一个叫做hello.html的文件在我的电脑上,然后我在我的电脑上安装一个叫Web服务器的软件,以及给我的电脑起个名字(域名)。这样,你就可以用浏览器通过http://www.xxx.com/hello.html 的方式来访问这个文件了。

为什么说这是上古时代呢?因为这个时候,仅仅是一个“静态”的文件。所谓静态,就是说,这个文件谁看起来都一样,我在实验室访问,你在家访问,他在寝室访问,这个文件都是这个样子的。静态文件直观上有两点缺陷: 1. 不能区分不同的用户,例如,如果这个页面内容要写hello, nius,所有人看到的这个文件都是hello, nius。显然,你不一定叫nius这个名字。 2. 网页难以维护。上新浪、淘宝,你会发现页面中很多地方都是一样的。但是,不同的页面,还是不同的文件呀。当我发布一个新的html文件的时候,我得把老的html文件拷贝一份,然后把不一样的地方换掉。例如我写这篇博客文章,其实只有标题、内容等几个地方不同,其他内容我就不希望管了。静态网页是做不到这点的。

这里有一个Web服务器的概念,服务器即可以指那台电脑硬件,也可以指软件。服务器其实和家用电脑没有太多不同,早期的服务器其实就是一台连着网线的家用电脑,上面安装了一个叫Web服务器的软件,一旦电脑关机了,网站就上不去了。直到现在你也可以这么干。当然,国内环境复杂,想这么干有接入网方式、行政因素来阻碍你。

暴发时代 2.0 后台模板架构

大家肯定觉得1.0的网站太无聊了,我连登录都登录不了。于是程序员们纷纷表示,放开html,让我来!于是各种语言开始实现web后台。例如PHP。PHP是专门为了写网页设计的程序语言。php能够在用户请求某个文件时(例如仍然是之前的html文件,但是后缀名改为php),把这个html文件中的一些内容替换掉。这样,页面中的广告就可以只在guanggao.php里写一次,当我请求一个index.php时候,直接把guanggao.php中的内容替换到相应的位置(相当于做了一个复制粘贴)。

这就是网页后台模板的概念。所谓模板嘛,就是可以动态的把其中的内容替换掉。

但这个时候,还是不能登录,因为用户名没法记录在php文件里呀。这时候,数据库就出现了。免费的数据库比较流行的是mysql。所谓的数据库,就是把你的用户名和密码存进去,这样,当你登录的时候,我比对一下密码对不对,对了,我就让你登录,在网页上显示 hello {你的名字}。

当然数据库能做的还有很多,例如这篇博客的标题、内容、标签,都存在数据库里。当你请求这篇博客的地址: http://nius.me/from-requirements-to-website-4/ 时,php语言判断出你请求的是哪一篇博客,再从数据库中取出博客的内容、标题等,拼装出一个html页面,返回给浏览器。这样,浏览器就可以显示html内容了。

很多人都听说过LAMP,也有很多培训班专门用来培训LAMP开发的,LAMP= linux + apache + mysql + php,linux是免费的操作系统,跟windows的角色类似,就是让电脑跑起来。由于免费、开源,定制性、稳定性极强,所以服务器市场上占了很大的份额。apache是之前提到的传说中的Web服务器,提供HTTP协议的实现。mysql是数据库,记录诸如用户信息、博客内容等。php则是动态语言,相当于把Web服务器和数据库连起来,从而实现动态网页。

但是,PHP并不是唯一的动态语言。Java也是很流行的后台语言。Java推出了J2EE等一些列方便程序员开发网页的类库,以及很多开源社区的框架,如著名的Struts+Sprint+Hibernate。这里要说一下框架是什么,框架就是避免程序员重复劳动的东西,因为web开发可以被抽象成MVC模式,所以可以根据这种抽象来把很多东西规范好,程序员只需要进行很少的开发,就能够完成很多内容。

除去PHP和Java,前几年流行Ruby、Python,这两年流行Node.js,都是相对成熟的可以用来开发Web的语言。但只要你把握住根本,其实Web后台开发就做了一件把数据变成html的事,就不难学会这些后台语言,至少很容易学会如何使用这些语言的Web框架。

来,一图抵千言。

后台模板架构

后现代派 2.1 前端模板架构

2005年左右,流行起一种Web开发方式成为AJAX。背后的概念比这个名词简单多了。上面说的,仍然是一个网页请求到后台,后台返回一个完整的网页。这样做的问题是,如果网速慢,网页又大,用户体验很不好。于是,可不可以就把网页变化的部分传过来呢?例如你正在看这篇博客,想看上一篇从需求到网站:(三)视觉设计,点击链接之后,仅仅刷新博客内容,其他东西都不用变,这样,网络传输的内容减少了,网站的响应速度提高了。

这个技术之前充其量是上面提到的后台模板架构的补充。直到他的膝盖中了一箭。

既然可以局部刷新网页,那为什么不干脆整个网站就一个页面,所有内容都进行局部的获取呢?Web先驱们早就想这么干了!Google的Gmail就是这样的,Gmail成功的使用制作网页的技术,做了一个能跟客户端程序媲美的Web应用程序。但是在Google做Gmail的时候,进行这种开发还是十分困难的,因为浏览器不够先进。现有的东西不够好,怎么办?程序员的思路当然是:自己做一个!于是Google就自己做了一个浏览器Chrome,一炮打响。

这里提到浏览器不够好,为什么浏览器不够好呢?这就涉及到Javascript和Html的问题。大量使用AJAX的网站,虽然减少了网络传输的带宽,但另外一件事必须得考虑,就是html文件不光在后台拼接了,还需要在浏览器里进行拼接。浏览器要把之前后台语言干的事接盘,自己来干。程序员把一种用Javascript这种语言写的代码,传输到浏览器里,让浏览器来对拼装html。这时浏览器就蛋疼了。本来我只要显示一下html就行了,这下倒好,我还得自己把html组装好,再来显示。特别是老旧的IE,性能跟不上啊,显示一个网页就把电脑给卡死了。不过新的IE9、10性能倒是蛮好的,微软还是挺厉害的。

在浏览器性能跟上之后,Web架构就可以放弃掉后台模板这种东西。Twitter就这么干了,做成了Single Page Application。就一个网页,网页内容全部通过Javascript根据后台传输的数据来生成。这样的好处是,网页显示extremely fast。但这带来的挑战是,前端Js的代码量大量增加,甚至赶上后台代码了。这下大家傻眼了,从前写网页前端没什么js代码,顶多几百行,基本想怎么写就怎写,这下要维护一不小心上万行的代码,怎搞?必须模块化,但是Js原生对模块化开发的支持并不好。于是开始有各种模块化标准和解决方案。

与此同时,前端的业务逻辑也十分复杂,模板这件事(就是动态的拼装html),也从后台转移到了前端,于是,前端开始出现了很多框架。还记得框架嘛?就是让程序员用很少的代码干更多事的东西。多到……上百种( ⊙ o ⊙ )!谈到Java,我们说SSH,谈到Ruby我们说Rails,谈到Python,我们说Django,谈到Javascript……简直太混乱啦!Backbone算是比较火的,但替代品一抓一大把,没有人敢说哪种最好。所以现在开发这种网站很头疼,选框架就让你选半天。

既然业务逻辑、模板都被前端做了,后台做什么呢?后台就只需要做数据持久化就行了。也就是说,后台其实就把前端传回的数据保存下来,当前端请求的时候返回过去,就OK啦。好幸福!后台人员再也不用学html了。返回的数据用JSON格式,你可以理解为key-value,就是什么键的值是什么,例如用户名-小明,就是一组key-value。如图~

前端模板架构

艾玛,还没讲CSS呢

Html是告诉浏览器,要显示哪些内容,而CSS则是告诉浏览器,显示哪些内容。例如,标题应该比正文大,段落之间应该空多少距离。CSS和Javascript在网站中都属于静态文件,还记得上文Web 1.0时代,静态html嘛?自从到Web 2.0,html就已经不是静态的了,是后台语言动态生产的,所以静态文件一般指Js、css、图片等等。你会发现,到最后,html文件没了,因为藏到前端模板(Js)里面去了。

之前就被骂写的太多了,感觉这篇文章又说多了。。。⊙﹏⊙b汗 各位海涵。

============ 本文是一个系列,其他已经写好的文章: