写给服务器端程序员的前端实践入门

一如往常,这篇文字的重点并不是语法。我一向认为能通过一次查手册就能明白的东西均非重点。

这篇文字号称入门,不过更多的是站在一个经验丰富的服务器工程师(随便说说)的角度,介绍如何引入现代化的前端工具链。以及我自己的项目习惯。

当然可能不一定符合您的实际情况。我会在后文详细描述适用场景,请酌情取舍。

说一下背景。

之前的一个项目,原来的页面展示方式是采用后端模板渲染,引入js,css。有动态元素的部分,就使用jquery锚定某个类或者id插入。事件的侦听挂在父节点上。

后端的每一个模板对应一个相应名字的js文件。css是一整个大css。

不要吐槽,我很清楚这样不好。但是这个项目的前端是我,在写这个项目之前,我对js没有任何前置了解。

但是从另一个方面来说,如果一个服务器端工程师,之前没有任何的经验,那么很可能写出这样的代码。

当然,本质上来说,这样的做法极容易导致前端代码混杂并且不受控。事实上也是。我在后面希望增加一些动画元素,但是发现都很难改。

痛定思痛,我决定重写掉模板和js。引入完整的前端工具链,使用新的前端框架来处理交互代码。

选择vue没有什么偏好的问题,你当然可以用react或者angular。我用vue是因为看着vue足够小。

然而作为重构,我不希望更改后端的逻辑。尤其是,我依然会使用后端来控制路由。

不过这里其实就会和大多数的框架文产生了背离。因为现在流行的趋势是用框架去做单页应用。后端只负责提供数据接口,前端控制路由。前端路由切换的实质是js动态删掉当前页面元素,再填充入指定路由的数据而已。

前端路由是个不错的主意,但是我依然不觉得比后端路由更好,尤其是在涉及鉴权的时候。

因为不会更改后端路由,所以后端模板也不会被替代。但是我需要利用前端框架方便的模板系统以及双向绑定。所以,在我的项目中,后端模板和前端模板并存。

不过,我这里的主意是,后端模板用于支持基本结构,以及静态化的东西,鉴权类的页面,以及为前端模板提供挂载点 — 当然,实际在,在我的项目中,前端模板是作为组件(components)的模式存在。组件只用来渲染动态的部分 — 实际上也就是把之前jquery的代码用更新的方式重写了而已。

但是不止是这样,事实上,经过考虑之后,我把分页也直接放在前端做了。后端在模板中写分页代码,并不是很优雅,我是这样想。

一段展示元素到底应该放在模板中还是组件中,除了哪些显而易见的部分,其他的,取决于个人的口味而已。

上面瞎扯的是自己的私货。

首先从项目结构开始,譬如以ui为根。

ui - /build
   - /js
   - /node_module
   - /src
     - components
     - js
   - /static
     - img
     - css
   - /templates
   - package.json
   - webpack.config.js

解释一下。

build用来放已经第三方某些不方便通过npm进行安装的包。这样的包意外不少。放在build中的js文件,理论上来说大多通过在后端模板中直接 <script src="xxxx">的方式进行引入。

js用来放编译好的js文件。不过,这个文件夹中文件是webpack打包后自动生成的,原则上不要修改。在js文件夹中生成的js文件,需要被通过 <script src="xxx">加入到模板中。

node_module用来存放js的依赖包。

src中分为两个文件夹,components和js,用来放组件和js源文件。之后这会成为webpack的入口。

template用来放后端模板。

static中的css需要特殊说明。我在这里面存入的实际上只有scss,webpack在打包的时候会自动解析。另外就是,这里的css,并不需要通过引入模板文件,而是依赖webpack把它和js打包这样的方式。所以需要在对应的js中require进来这个scss。

package.json可以类比于python的requirements或者java中的pom.xml

webpack.config.js是webpack的配置文件。关于webpack,后文会说。

然后是webpack。

webpack是一个打包工具,类比的话类似于……链接器?不过或许智能点。根据你给定的入口,会去分析reuqire或者import,然后把对应的部分加载进来。

不过稍微特殊一点的是,webpack对于require很宽容。在js代码中,require本来只是用来加载js模块的。但是作为webpack入口的js文件,能require任意webpack支持的格式。

所谓webpack支持,也就是你设置了其对应格式的loader。看起来,webpack会分析require中后缀,然后尝试用不同的loader去加载。如果你设置了scss loader,那么就可以require('xxx.scss')了。

webpack是一个好用的工具,除了loader,webpack还提供了丰富的插件以供预处理,后处理的。不过,稍微有些不好的是,webpack的文档比较不佳。只能说姑且能费力找到您需要的内容。

不过,值得庆幸的是,最简单的webpack.config.js的模板就足以应付大多数项目了。

var webpack = require("webpack");
var path = require("path");
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        example: "./src/js/example",
    },
    output: {
        path: path.join(__dirname, "js"),
        filename: "[name].bundle.js",
        chunkFilename: "[id].chunk.js"
    },
    module: {
        loaders: [
            { test: /\.vue$/,    loader: 'vue'},
            { test: /\.jsx?$/,   exclude: /(node_modules|bower_components)/, loader: 'babel?presets[]=es2015'},
            { test: /\.json$/,   loader: "json" },
                        { test: /\.css$/,    loader: "style!css" },
                        { test: /\.scss$/,   loader: "style!css!sass" },
                        { test: /\.png$/,    loader: "url-loader?prefix=img/&limit=5000" },
                        { test: /\.jpg$/,    loader: "url-loader?prefix=img/&limit=5000" },
                        { test: /\.gif$/,    loader: "url-loader?prefix=img/&limit=5000" },
                        { test: /\.ttf$/,    loader: "file-loader?prefix=font/" }
        ]
    },

    plugins: [
        new CommonsChunkPlugin({
            filename: "commons.js",
            name: "commons"
        })
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false
             }
         })
    ]

}

上面这个webpack.config.config, 当然首先需要您安装了webpack,安装这部分,官方文档写得足够清楚。

entry指定了入口,这是一个对象,是支持多入口的。如果是后端控制路由,整个应用会是一个多页面应用,这时候,多入口几乎是一定的。

output不用改。这样会生成一个common.js和 xxx.bundle.js。需要注意,使用<script>引用的时候,common.js需要在前面。common.js正如名字所说,是公用的部分。xxxx.bundle.js是对应页面打包好的js。这个js里面,包含了css代码,希望您还记得这些事情。

module中主要是loader,我在这里指定了很多loader,用来应付各种情况。需要注意,这些依赖都需要通过npm install xxxx --save 来安装的。

我加入了两个插件。第一个是用来把common部分打成common.js的插件。这个基本都会有。

第二个是UglifyJsPlugin,用来混淆和压缩js文件的大小。实参压缩后是原来的1/3左右。这个原理实际上就是抽源代码AST,然后改变量名为不冲突的最短字符应该是。

以上的配置请酌情更改,能应付很多情况。

另外推荐代码风格使用es6。正如前文所述,webpack因为加载了babel-loader,所以能自动转换es6风格的代码。es6的相关内容不累述,教程和资料很多。不过如果是python程序员大概会觉得es6风格很亲切……飘荡着浓厚python臭味的js,我的印象是这个。

不过写着挺爽。

然后现在说Vue。

有一个坏消息。

单页面应用由于只有一个页面,那么只需要初始化一个ViewModel,也就是Vue的实例。因为在整个交互过程中的context都是同一个。

但是,如果后端控制路由。每一次页面的跳转都是页面的整个切换,是无状态的。index页跳到detail页,其context当然不是同一个。所以一个很浅显易懂的事实是,我们需要多个ViewModel。

index.html中我们需要new Vue, detail也是,confirm也是。VM的初始化负担稍微有些大,有可能会拖速度。不过说实在的,我并没有这个感觉。

对于Vue我很中意它的写作风格 — 组件的script,css,js都写在同一个文件。我觉得这是一个很好的趋势和做法,能显著地降低维护成本。所以对于写组件而言,我推荐这样做。

但是模板不行。我总是推荐模板的js和css和引入按照以下steps来做。

假设现在我们有index.html模板,我推荐在src/js中增加index.js文件,static/css下增加index.scss。写好了之后,index.js中追加require('index.scss')。执行打包,在js下生成index.bundle.js。在index.html中,通过<script>引用过来。

对于Vue本身,我没有什么特别想说的。文档已经很全了,照着试试就明白怎么用。双向绑定是一个很好的功能。

稍微提一下v-if,v-else,v-show。

实践证明,v-if, v-else不支持嵌套。如果有嵌套需求,把需要内层嵌套的抽出来独立作为一个组件,然后通过挂载点挂载上去。

不过这样其实有时候很尴尬,为了实现几个嵌套的v-if,组件深度常常出现四层以上。写起来很烦人。

六月十五日补充:

想说说分页的事。

目前我的方案是在前端分页。

至于理由有三。第一是能做到api足够简单,单纯返回全量数据。第二是本来现在数据就少,一个百K不到的json对网速也没什么显著影响。第三是这样切换下一页上一页更加流畅,毕竟不需要再去请求后端接口。

之后如果有改进,估摸就是在后端做一个limit参数来稍微限制一下。

vue的分页插件轮子不少,遗憾的是即使我使用其中star数最高的项目,依然无法顺利工作。在向作者提了多个issue,并且等了四天之后,我决定自己开发一个vue插件。

这个插件的使用方式借鉴了vue-paginate这个项目,但是不管怎么说,我有些嫌弃其方法命名的不优秀以及语法的别扭。另外,对方在处理异步数据载入的时候,很明显是一个patch,我不是很心水这样将就的用法,而是更希望参数传递以及异步载入这两种方式在使用的时候没有本质区别。

这个轮子的名字叫做vue-paginator, github上同名的项目居然有好几个,而且还有vue-pagination或者vue-paginate,大家还真是不嫌折腾。

项目地址 vue-paginator

这个项目是pure js,而不是es6语法然后需要编译。我考虑过后者,一个显著的好处是后者写起来更类似我平时的代码风格。但是后来还是放弃了,我不是特别希望引入一个新的依赖。

vue插件的写法配合教程应该能看懂。不过我这里想提一下,有可能的一个坑。

我提供了一个page()的方法,当然还有hasPrev()和hasNext()。

其实这里稍微有些别扭,譬如,这三个方法都是无参函数,为什么我不直接写成属性呢?比如, page, hasPrev, hasNext。

是的,这三个在最开始的实现中,的确都是以属性的方式存在。

但是后来我放弃了,原因在于,我发现对其的更改,无法被监听。

这个其实很奇怪,按照我的理解,以及按照官方文档所告知的,属性变更的通知实际上原理是这样:

  1. 用户定义一个对象

  2. vue设置set和get方法在所有的属性上

  3. 在set方法上设置watcher的触发器

  4. watcher得到触发,重新求值

但是,我发现,我对于$set的属性,进行复制这个过程,watcher并没有收到这个更改的通知。

我尝试过$(set), this.vm.xxx=xxx, this.vm.set(xxx, xxx)等我认为可以让事件触发的方式。但是实际上没有。

所以后来只能妥协,升其为方法。因为方法是一定会被强制重新计算的。事实上也是,这样更改之后,工作良好。

这篇文章写得很随便,姑且随便看看。有意见我会改。

AS。