JavaScript 中的运算操作陷阱

最近做JSP大作业,需要写一个网站,其中用到了分页系统,就自己撸了一个。

其中的翻页逻辑是这样写的:

1
2
3
4
5
6
7
8
/*上一页*/
previousPage.click(function(){
clickPage(grids[currentActivePos].find('a').html() - 1);
});
/*下一页*/
nextPage.click(function() {
clickPage(grids[currentActivePos].find('a').html() + 1);
});

扔到网页里实验,发现[上一页]功能没有问题,但是点击[下一页]之后没有反应,下边还有一个类似的错误:

1
grids[i].find('a').html(index + i - 3);

当测试时,index是从.html()方法获取的字面值为3的参数,i是循环控制变量,当i也是3时,出现了gridsp[i]的值变成30而不是3的情况。

一直想不明白这奇怪的现象是因为什么,但问题肯定就出在这句话上,所以就试着把表达式的顺序变了一下,像这样:

1
grids[i].find('a').html(i - 3 + index);

发现结果变成了03,到这里才明白这结果不是数值而是字符串!也就是index是string类型的,直接用数字和它进行+操作结果不是进行数值运算而是字符串连接!

因而也明白了翻页逻辑的错误在哪里。改动如下:

1
2
3
4
5
6
7
8
9
10
11
/*上一页*/
previousPage.click(function(){
clickPage(grids[currentActivePos].find('a').html() - 1);
});
/*下一页*/
nextPage.click(function() {
var des = eval(grids[currentActivePos].find('a').html() + '+ i');
clickPage(des);
});
/*先进行减法运算,把表达式结果转换成数值类型*/
grids[i].find('a').html(index - 3 + i);

总结:非常傻的一个错误,找到原因之后觉得自己非常基础的东西没有掌握牢固。这一段时间虽然涉及的面不少,但是都没有什么深入,这样就有点像”非科班”的CS学生了,应该警醒。

我的网站

终于找到相对靠谱的服务器了,域名:

rucer.cn

意思是:人大人.cn,果然人大是文科性质的学校,这么靠前的域名竟然没人注册。

旧版网站比较LOW,就不放上去了,之后有时间做新版。

处理运行时状态的改变

英文原文地址 http://developer.android.com/guide/topics/resources/runtime-changes.html

一些设备配置会在运行时改变(比如屏幕方向、键盘可用性和语言)。当发生这些改变时,Android会重新启动正在运行的Activity(onDestroy()会被调用,之后是onCreate())。设计重新启动行为是为了通过自动重新加载你的应用从而适应新的配置,重新加载的应用会采用适应新配置的替代资源。

要正确地处理重新启动,非常重要的一点是你的activity在正常的Activity活动周期中存储下来它之前的数据,在活动周期中,Android在销毁你的activity之前会调用onSaveInstanceState()从而你可以保存有关应用状态的数据。你可以在onCreate()或者onRestoreInstanceState()中恢复数据。

为了测试你的应用是否状态完好地重新自启动,你应该在应用进行各种任务时改变配置状态(比如修改屏幕方向)。为了能够处理诸如配置改变或者用户接了一个电话,之后在你的应用进程被销毁之后回到应用之类的情况,你的应用需要能够在保证用户数据或状态无损的情况下随时重启。如果你想知道如何恢复activity的状态,请阅读Activity lifecycle.

但是你可能会遇到这样的情况:重新启动应用并恢复大量的数据开销非常大,而且会导致糟糕的用户体验。在这种情况下,你有另外两个选择:

  1. 在配置改变期间保留一个数据对象
    允许你的activity在配置改变时重启,但是要为你的activity保留一个有状态的对象(object)。
  2. 你自己来处理配置的改变
    在特定的配置改变时阻止系统重启应用,但是当配置改变时要接收回调函数,从而你可以按照需求手动地更新的你的activity。

在配置改变期间保留一个数据对象

如果重启应用需要你恢复大量的数据,重新建立网络连接或者进行其他密集型操作,那么由于配置改变而造成的一次完整的重启可能会是一次非常慢的用户体验。除此之外,你可能也无法使用系统通过onSaveInstanceState()回调函数为你保留的Bundle完整地恢复你的activity状态——它并非是被设计用来承载比较大的对象(比如bitmaps)的,而且其中的数据必须被序列化和反序列化,这些操作会占用非常多的内存并且使得配置切换缓慢。在这种情况下,你可以通过持有一个Fragment来减轻因为配置改变而重启activity时的负担。这个fragment将会保存你想要留存的有状态的对象的引用。

当安卓系统因为配置改变而关闭你的activity时,你标记为保留的fragment不会被销毁。你可以在你的activity中添加这样的fragment来保存有状态对象。

要想在运行配置改变期间在fragment中持有有状态对象:

  1. 继承Fragment类并声明对你的有状态对象的引用。
  2. 当fragment创建时调用setRetainInstance(boolean)。
  3. 把这个fragment添加到你的activity中。
  4. 当activity重启之后通过FragmentManager获取这个fragment。

举个例子,像下边这样定义你的 fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RetainedFragment extends Fragment {

// data object we want to retain
private MyDataObject data;

// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}

public void setData(MyDataObject data) {
this.data = data;
}

public MyDataObject getData() {
return data;
}
}

小心:虽然你可以存储任何对象,但是永远也不要传递一个绑定在Activity上的对象,比如Drawable,一个Adapter,一个View或者任何一个关联到Context上的对象。如果你这么做的话,会导致原有activity实例所有视图和资源的泄露。(资源泄漏是指你的应用一直挂起这些资源使他们无法被垃圾回收,因而很多内存会流失掉。)

然后使用FragmentManager把fragment添加到activity。当activity在配置改变期间重新启动后你可以从这个fragment获取数据对象。例如,像下边这样定义你的activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MyActivity extends Activity {

private RetainedFragment dataFragment;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}

// the data is available in dataFragment.getData()
...
}

@Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}

在这个例子中onCreate()添加了一个fragment或者恢复了对它的引用。onCreate()也在fragment实例中存储了有状态对象。onDestroy()更新了持有的fragment中的有状态对象。

你自己来处理配置的改变

如果你的应用在特定的配置改变时不需要更新资源并且由于性能限制不允许重启activity,那么你可以声明你的activity自己来处理配置改变,这样可以阻止系统重启你的activity。

标注:亲自处理配置改变将会比使用可选资源困难得多,因为系统不会自动为你应用这些资源。当你一定要禁用由于配置改变而造成的重新启动时,这项技术可以被看做最后的手段,对于大多数应用并不推荐。

为了声明你的activity要处理一项配置更改,在你的manifest文件中编辑合适的标签来修改android:configChanges属性,填入合适的值以代表你要处理的配置。可选的值列在了android:configChanges属性的文档中(最常用的值是”orientation”来避免屏幕方向改变时activity的重启,以及”keyboardHidden”来避免键盘可用性发生改变时activity的重启)。你可以通过使用 | 把值隔开的方式声明多个值。

举个例子,下边的manifest代码声明了一个同时处理屏幕方向改变和键盘可用性改变的activity。

1
2
3
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">

现在,当这些配置的其中一个发生改变时,MyActivity不会重启,而是接收到了一个对onConfigurationChanged()的调用。这个方法传递了一个Configuration对象,指定了新的配置。通过读取Configuration中的字段,你可以定义新的配置并且通过更新你的界面所用资源来做出合适的修改。当这个方法被调用时,你的activity的Resources对象会被更新并返回基于新配置的资源,从而你可以在系统不重启你的activity的情况下重置你的UI的元素。

注意:从Android3.2(API 版本13)开始,当设备在横屏和竖屏之间进行切换时”screen size”也会发生变化。因此在做API版本13或者更高(在属性minSdkVersion和targetSdkVersion中声明)的开发时,如果你想避免因为屏幕方向改变而引起的重启,你必须在”orientation”的基础上包含”screenSize”属性。也就是说,你必须声明android:configChanges=“orientation|screenSize”。但是如果你的应用是面向API版本12或者更低的,那么你的activity总能自己处理这种配置改变。(即使运行在Android版本3.2或者更高的设备上,配置改变也不会引起activity重启)

例如,下边的onConfigurationChanged()实现检查了当前设备的屏幕方向:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);

// Checks the orientation of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
}
}

Configuration对象表示出了目前所有的配置,不仅仅是已经改变了的。大部分情况下,你并不关心你的配置是如何改变的以及它是如何简单地重新分配你的资源以达到为你正在处理的配置提供备选的目的。例如,因为Resources对象现在已经更新,你可以通过setImageResource()重新设置ImageView,你也可以设置其他对已经使用的配置来说合适的资源(就像Providing Resources说的那样)。

注意Configuration中的字段都是整型,并且他们都与Configuration类中的常量相匹配。你可以参考Configuration中具体的部分来获取每个字段使用的常量。

记住:当你声明你的activity以处理配置改变时,你有责任重新设置你为之提供备选的每个元素。如果你声明你的activity来处理屏幕方向变化并且有需要在横屏和竖屏之间切换的图片,你必须在onConfigurationChanged()中为每个元素重新分配新的资源。

如果你不需要基于这些配置修改做activity的更新,你可以不去实现onConfigurationChanged()。在这种情况下,配置更改前后使用的资源是相同的,你仅仅是避免了activity的重新启动。但是,你的应用应该总是能够关闭后再打开时原样保留之前的状态。因此,在正常的activity生命周期内,你不应该把这项技术当成保留状态的选择。不仅是因为你不能阻止一些其他的配置改变引起的应用重启,而且你还应该处理诸如用户离开应用,在用户再次开启应用之前应用就被销毁的情况。

想了解更多关于你在activity中可以处理的配置改变,请查阅android:configChanges文档和Configuration类。

Python 进行 Web 开发

假期没事学python,因为之前一直在搭个人网站,后台用的是PHP,所以想用Python重写一下后台。关于python开发web应用,网上有一大堆教程,最多的是推荐用apache加载mod_python这个模块,看了下官网,13年停止更新了,而且对Python 3的支持很差,所以并不推荐这个。类似的有mod_msgi,这个可以看做是mod_python的继任者,但是配置起来尤其是在windows上配置起来非常麻烦,初学者做起来可能会比较痛苦。推荐使用Django框架开发,关于服务器官网里有这么一段话:

You’ve started the Django development server, a lightweight Web server writtenpurely in Python. We’ve included this with Django so you can develop thingsrapidly, without having to deal with configuring a production server – such asApache – until you’re ready for production.

也就是Django自带了纯python编写的服务器,非常方便,在测试阶段和小规模的开发阶段,用这个足以满足需求。关于教程,强烈推荐去官网硬着头皮读英文原版(注意教程版本要和自己下载的Django一致)而不要去看翻译完的中文版,因为Django更新很快,而且时不时有大的变动,看比较老的版本的话有时候回给自己造成莫名其妙的困扰。

Django官网:https://www.djangoproject.com/

Python下载:https://www.python.org/

下载完之后,先把python.exe的目录放到环境变量的Path下(Windows系统),然后命令行进入Django目录,执行 [ python setup.py install ] 即可。安装Django成功之后,在Django官网中按照教程一步步来就可以了。

Android ListView 和 ScrollView 冲突问题

最近做一款APP,其中有一个类似微博的评论功能的界面,先是列出微博的正文内容和图片等,然后下边是评论。一开始就想着用一个ScrollView把主要内容和评论区的ListView包起来,然后添加各个控件的内容即可(对,感觉上有点像这个CSDN博客的编辑界面嘛),但是写出来之后发现ListView只显示出了一个条目的高度,并且不能滑动,网上搜了一下发现原因是ScrollView和ListView都是可滑动的,把它们放在一块会有冲突,最后还是ScrollView获得了焦点,ListView不能滑动。网上的解决方法最多的是在加载ListView时用getMeasure计算每个条目和分割线的高度,然后相加,把结果设置为ListView控件的高度,不过貌似是只适用于ListView每个条目高度都一样的情况(没有试过,很奇怪为什么会这样)。要么就是自定义一个继承自ListView的控件,也是事先设置好ListView的高度,但这样总归比较麻烦,而且准确度不如由系统自己构造好。

懒癌发作实在不想自己去做这些事情,于是便想试一下比较投机的方法,就是在ListView的Adapter的getView方法中根据position构造不同的界面,即如果position是0,则用原来主要信息(微博正文,图片)的xml文件取inflate convertView,否则就用评论条目的xml去inflate,经试验果然可行。之后不死心想看下有没有更好的实现方法,去overflow上找了一下,发现有人推荐的方法和我的差不多,所以认为这种方法是比较好的,不需要做额外的工作,只需要把inflate的工作由主Activity放在Adapter里就可以了。

getView方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
/*主信息界面*/
if(0 == position)
{
MainHolder holder = null;
convertView = inflater.inflate(R.layout.info, parent, false);
holder = new MainHolder();
convertView.setTag(holder);
······
······
}
/*评论界面*/
else
{
ItemHolder holder = null;
convertView = inflater.inflate(R.layout.item, parent, false);
holder = new ItemHolder();
convertView.setTag(holder);
······
······
return convertView;
}
}

Android Resources$NotFoundException: String resource ID #0x1

今天写一个新界面,需要从其他界面通过intent传来参数,就先拿到之后通过TextView的setText显示来看看传参是否正确,写法:TextView.setText(arg) (arg是int型),然后就玩命报错 “android.content.res.Resources$NotFoundException: String resource ID #0x1”,一直不知道怎么回事,看过API才明白当调用setText()方法时如果传入int型是不会被当成内容而是resourceID来使用!R文件里当然不会有我的参数,所以报错!解决方法:TextView.setText(“” + arg)

Android FragmentTransaction 在不同版本下的奇怪问题

目前在做的一款APP,用到了一个布局块中的Fragment的切换,其中一个按钮按下后的事件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
btnTranslate.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
/*当前界面不是翻译界面时才进行下列动作*/
if( MAIN_FRAGMENT_ID != currentFragmentId )
{
setChoosedBtnState(MAIN_FRAGMENT_ID);
FragmentManager fragmentManager = getFragmentManager();
if( null == fragmentTranslate )
fragmentTranslate = new MainUI();
hideCurrentFragment(fragmentTransaction);
fragmentTransaction.show(fragmentTranslate);
fragmentTransaction.add(R.id.fragment_content, fragmentTranslate, "MainUI");
fragmentTransaction.commit();
currentFragmentId = MAIN_FRAGMENT_ID;
}
}
});

fragmentTransaction.commit();currentFragmentId = MAIN_FRAGMENT_ID;}}});
1
2
hideCurrentFragment(fragmentTransaction);
fragmentTransaction.show(fragmentTranslate);

这两句本来是为了提高切换速度,并且保存用户产生的数据而选用隐藏/显示来代替replace(remove/add),运行时在Android 4.0的模拟器和我Android 4.1的手机上没有任何问题,效果也很好,但是当我把程序放在我的Android 4.4(Galaxy S5··貌似这个机型问题挺多···)时出现闪退!具体情形是,当我点击一个按钮创建了一个Fragment的实例时程序正常运行,但是当我再次点击已经创建过实例的界面按钮时就会闪退,一直不明白是怎么回事,查了API也没找到相关的说明。

搞了好久,最后看到

1
<pre name="code" class="java">fragmentTransaction.add(R.id.fragment_content, fragmentTranslate, "MainUI");

这句话,它作用就是把产生的Fragment实例加入到指定的布局块中,再联想我的问题,也就是在加入一次然后再次加入出现了问题,所以试着只在第一次创建Fragment实例时调用这条语句,果然问题解决!

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
btnTranslate.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
/*当前界面不是翻译界面时才进行下列动作*/
if( MAIN_FRAGMENT_ID != currentFragmentId )
{
setChoosedBtnState(MAIN_FRAGMENT_ID);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
if( null == fragmentTranslate )
{
fragmentTranslate = new MainUI();
fragmentTransaction.add(R.id.fragment_content, fragmentTranslate, "MainUI");
}
hideCurrentFragment(fragmentTransaction);
fragmentTransaction.show(fragmentTranslate);
fragmentTransaction.commit();
currentFragmentId = MAIN_FRAGMENT_ID;
}
}
});

Android Error: No resource found ···

关于 android:id=”@+id/” 和 android:id=”@id/” 的问题

本以为自己已经理解了这两个,无非就是第一次创建id时用+id/呗
但是没想到这里创建顺序是严格按照代码顺序来的,如:在RelativeLayout中

1
2
3
4
5
6
7
<RelativeLayout
...

<Widget1 android:layout_above="@id/widget_2" .../>
<Widget2 android:id="@+id/widget_2" ... />

<RelativeLayout/>

这样写会报错,大致说是Widget1找不到widget_2!!!!

原来需要在Widget1中这么写:android:layout_above=”@+id/widget_2″
也就是在第一次遇到这个id时就创建它,放心,这个id还是Widget2的,只是先被Widget1拿去创建并引用了而已

【PHP学习】move_uploaded_file() 使用注意

今天刚弄好WAMP,端口问题焦头烂额一上午,最后停止IIS服务就好了。。。

看PHP基本的部分,在用到 move_uploaded_file()时一直是错误,说是无法把文件从临时路径移动到指定路径下,最后发现指定路径不存在。。。就是最后的upload目录没有创建,心里还想着它会自动创建这个目录呢,真是傻了。。。

(代码是W3SCHOOL上的例子,稍有改动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
if( $_FILES["file"]["type"] == "text/plain" && $_FILES["file"]["type"] < 20000 )
{
if ($_FILES["file"]["error"] > 0)
{
echo "Error: " . $_FILES["file"]["error"] . "<br />";
}
else
{
$oldPath = $_FILES["file"]["tmp_name"];
$newPath = "D:\\WAMP Server\\wamp\\tmp\\upload\\" . $_FILES["file"]["name"];

echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Stored in: $oldPath temporarily!<br />";

if( file_exists($newPath) )
{
echo "$newPath already exists!";
}
else
{
move_uploaded_file($oldPath, $newPath);
echo "Stored in: $newPath !";
}
}
}
else
{
echo "Invalid File!";
}
?>

青城山

他提了提书箱,有点潮,昨夜下了整晚的雨。“听人说长安是个好地方,让人去了就不想回来”,他心里想着,望了望背后的青城山。青城山再往后,地上是有两行脚印的,只是雨太大,早就冲刷干净了。

路上虽然辛苦,但是也见到了不少人和事,果真是读万卷书不如行万里路。

他虽然读了很多书,写了很多文章,但是这次仍然控制不住拿笔的手的颤抖,尽管文章在心中已经想好,但是笔还是未落又起。“墨又干了”。他端起水倒到砚台上,没有拿起墨,而是用笔在砚台上来回涂抹,直到已经干掉的墨和清澈的水再也不分彼此。

他不知道在害怕什么,因为当前最好的选择就是把肚子里的文章倒出来,然后一身轻松地离开,等到挤着别人与被人挤着的时候踮踮脚探探头然后在那张大纸上看到自己的名字,然后等着他挤的人与挤他的人的五颜六色的目光与五颜六色的声音。

哦,他想起来为什么害怕,因为他并没有想过如果那张纸上没有自己的名字的情况,他是村里最谦逊也最有学问的书生,戒尺不离手的先生和脏话不离口的父亲都很喜欢他,他是最谦逊的书生,但也是最有学问的,所以他敢在先生和父亲面前,他敢在她面前说自己一定能被写在那张纸上,并且还是在一眼能看到的位置。

墨已经可以用了。“盛世黎民,嬉游于光天化日之下。太平天子,上召夫景星庆云之祥”。他是见过考官的,考官很喜欢他的文章,考官说过一定不能错过任何人才。他觉得自己就是考官说的人,因为考官说这话的时候是望着他的,他没有礼物给考官,考官说也不收礼物,他拿出了自己的文章,考官很喜欢他的文章,考官说的。

现在他在写了,考官很喜欢他的文章,考官说的。他写的时候没有想着这篇文章该怎么写,因为早就已经想好了,他想着先生的戒尺,父亲的脏话,青城山后的她,和乡里人五颜六色的目光。他没有想到考官,因为考官很喜欢他的文章,考官说的。

“天地交泰,斯称盛世。”落完最后一笔,他没有把笔放回笔架,而是重重地戳在了砚台上,溅出来的墨已经磨过好几次,因而黑的发亮,他白色的衣服和白色的宣纸也有几处黑的发亮了。

长安城真的很大啊,走上一圈比爬上青城山还累。可能是只有自己一个人逛这长安城吧,没人说话,也就只能想着路有多长,脚有多累。他想找个地方扔掉书箱,因为用不到了。他要赶快翻过青城山到背面去,不能再背这么重的东西了,他要翻过青城山等着贴那张纸的人的到来。他在想如果见到了那个人,一定要拿点银子出来说声官爷辛苦了。

他现在不害怕如果那张纸上没有自己名字的情况了,因为他早就知道蓬莱岛上有神仙,神仙是不用为这些世俗之事烦心的,他如果不能被写在那张纸上,那他就去找神仙。反正总是有无量的前途的。

他想到了她,她是不会和他一起去当神仙的,她想住进大房子里,她想住进长安城,她不想住到蓬莱岛上。他不知道她会不会阻拦他去当神仙,但是她是送他到了青城山下的,她是送了他去长安城的。神仙是自在的,神仙不会死,所以他不着急去当神仙,他想先在那张纸上看到自己的名字。

他提了提书箱,有点潮,昨夜下了整晚的雨。“总是听爹说长安是个好地方,很大而且人很多,走上一圈像爬青城山一样,让人去了就不想回来”,他心里想着,望了望背后的青城山,他知道娘也在山背后看着青城山。他不想去写文章,他想当木匠。读过很多书的爹在听到他的志向后也开始脏话不离口了。他问过爹长安城的妓院,就是书里写的青楼。爹说没有文章里写的那么好,爹说他没进去过,他在外边看的。他说要帮爹进去看看,爹难得没有骂脏话,反而惬意地笑了起来,背起一段他没听过的古文。

爹说过到长安城一定要去找那个考官,因为那个考官很喜欢爹的文章,爹还说在爹去过长安的那一年后,考官的儿子就住在了长安城。爹说了很多那个考官的好话,他也相信。

“但是,蓬莱岛到底在哪呢?其传在渤海中,去人不远···盖尝有至者,诸仙人及不死之药皆在焉。”“那渤海又在哪呢?村头李木匠的家比渤海好找多了,而且他做的事比神仙还自在,他的女儿也比仙女还好看。爹不也是当了教书先生,娶了他先生的女儿吗。我就和爹一样吧。”他在从贴那张纸的地方赶回家中的路上这么想着,等着爹的脏话和娘的眼泪,他知道这是因为爹去不了青楼了,娘也住不到长安城了。他觉得自己终于可以当木匠了,他准备先到山上去选好一棵粗壮的树当材料,他还带了墨线,因为只有李木匠家里有墨斗,自己只能先用一根墨线了。

他终于可以真正快乐了,他没有当上木匠,但是他和李木匠打造出来的家具永远在一起了,他作为一件家具永远和李木匠的女儿在一起了。


真是:

凡缘往事伴云消,
苦海命泉渡神桥。
红尘修真得证道,
蓬莱仙境乐逍遥。