vue、webpack、express、mongodb搭建简单跑通全栈项目

project.png

本文会使用vue搭建一个简单单页应用,并且使用webpack打包,使用express框架服务,并使用mongodb管理数据存储,在服务器端部署,可以实现ip访问

项目效果预览

http://119.29.208.124:8080/#/

环境前提

Node.js (>=4.x, 6.x preferred), npm version 3+ and Git.

不论是本地开发还是服务器部署,都需要node环境,次处就不细讲如何在windows和linux配置nodejs环境了

1.vue-cli快速搭建项目

安装

1
npm install -g vue-cli

创建集成了webpack的项目

1
vue init webpack my-project

接下来会进入安装阶段

1
2
3
4
5
6
7
8
9
10
11
Project name (my-project) 
//项目名称,可以自己指定,也可直接回车,按照括号中默认名字(注意这里的名字不能有大写字母,如果有会报错Sorry, name //can no longer contain capital letters),阮一峰老师博客为什么文件名要小写 ,可以参考一下。
Project description (A Vue.js project) //项目描述,也可直接点击回车,使用默认名字
Author (...)
//选择部分
Runtime + Compiler: recommended for most users //运行加编译,推荐选择
Install vue-router? (Y/n) //是否安装vue-router,这是官方的路由,比较适合构建单页应用,于是我在此处选择了使用
Use ESLint to lint your code? (Y/n) //是否使用ESLint管理代码,ESLint是个代码风格管理工具,是用来统一代码风格,新手建议不用,不然回多出来很多语法上不规范而引起的错误
Setup unit tests with Karma + Mocha? (Y/n) //是否安装单元测试,此项目没用到

Setup e2e tests with Nightwatch(Y/n)? //是否安装e2e测试 ,此项目没用到

project.png

  • build目录 npm build *运行时执行的文件存放在此处,还有很重要的webpack配置文件
  • config目录 确定了执行run dev 和run build时的一些配置文件
  • dist目录 存放的是build命令执行后生成的产品文件
  • node_modules目录 存放的是依赖
  • src目录 存放的就是源码

在cmd里面切换到项目目录

1
cd my-project

安装依赖,此时才会生成node_modules目录

1
npm install

这个命令会自动读取package.json里面含有的所有的依赖信息和版本

等待安装完毕,就可以尝试启动项目了

1
npm run dev

这个时候,会自动拉起浏览器,自主访问http:\\\localhost:8080

此时,在这个页面会出现一个vue的默认界面
vue.png

此处建议使用chrome浏览器,安装一个vue-tool插件,会比较方便的进行vue开发

接下来就可以进入到我们的项目开发阶段

2.完成本地环境下项目开发

由于涉及到xhr请求,所以还需要下载vue-resource,样式方面选择了bootstrap框架,所以先安装依赖

1
npm i vue-resource bootstrap --save

然后我们需要在main.js中引入我们的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'

//需要我们引入的
import VueResource from 'vue-resource'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js'
Vue.use(VueResource);

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})

2.1统一部分编写

我们可以看到src源码目录的结构如下

dir.png

我们可以把app.vue当做是主组件,其他组件的入口,所以在app.vue编写的部分,在每个路由下的页面都会存在

,我们可以在此处,为我们的项目编写一个导航栏和页脚

此时的app.vue

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<template>
<div id="wrapper">
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/home">
<i class="glyphicon glyphicon-check"></i>
丁丁
</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav" >
<li><router-link to="/home">首页</router-link></li>
<li><router-link to="/vedios">视频</router-link></li>
<li><router-link to="/books">书籍</router-link></li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">搜索</button>
</form>
</div>
</div>
</nav>
<div class="container">
<div class="col-sm-12">
<router-view></router-view>
</div>
</div>
<div class="footer">
<div class="container">
<div class="col-sm-3">
<ul>
<li class="li-head">产品</li>
<li class="li-item"> <router-link to="/books">视频</router-link></li>
<li class="li-item"><router-link to="/vedios">书籍</router-link></li>
</ul>
</div>
<div class="col-sm-3">
<ul>
<li class="li-head">关于</li>
<li class="li-item"> <router-link to="/about">了解我们</router-link></li>
<li class="li-item"><router-link to="/about">加入我们</router-link></li>
</ul>
</div>
<div class="col-sm-3">
<ul>
<li class="li-head">服务支持</li>
<li class="li-item"> <a href="www.decadexun.cn">技术支持</a></li>
<li class="li-item"><router-link to="/books">售后服务</router-link></li>
</ul>
</div>
<div class="col-sm-3">
<div class="head">100-800-200</div>
<div class="box"> 联系客服</div>
<div class="time">周一至周日 9:00 - 22:00</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'app'
}
</script>

<style>
.footer{
border-top:#e7e7e7 1px solid;
background: #f8f8f8;
padding: 30px 0;
}
li{
list-style: none;
}
.li-head{
font-size: 18px;
margin-bottom: 10px;
}
.li-item{
margin-top:10px;
}
.box{
width: 100%;
height: 30px;
background: #337ab7;
margin: 10px 0;
text-align: center;
line-height: 30px;
color:#fff;
}
.head{
font-size: 20px;
font-weight: bold;
text-align: center;
}
.time{
font-size: 14px;
text-align: center;
}
</style>

由于用到了bootstrap的自适应合并、轮播图组件,所以,如果此时你run dev,会报一个缺少jQuery的错误,所以,我们可以去网上找一个jquery的cdn库,找到根目录的index.html,这是我们的首页入口页面,加入如下内容

1
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>

由于bootstrap是一个可以自适应手机端的ui库,所以你也可以顺手加入

1
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

于是在手机上访问的时候,也会 有较好的效果

此时的index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>首页</title>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

然后

1
npm run dev

此时的效果

index1.png

2.2创建首页组件

看到上图的中间部分,这就是<router-view></router-view>渲染的效果,此时的首页默认是components下的vue组件,于是我可以写一个自己的首页组件,取代中间的部分

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<template>
<div>
<div class="col-sm-9">
<div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
<!-- Indicators -->
<ol class="carousel-indicators">
<li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
<li data-target="#carousel-example-generic" data-slide-to="1"></li>
<li data-target="#carousel-example-generic" data-slide-to="2"></li>
</ol>

<!-- Wrapper for slides -->
<div class="carousel-inner" role="listbox">
<div class="item active">
<img src="http://res.cloudinary.com/dyb29pfpm/image/upload/v1507608949/1507513417919_h1vcov.jpg" alt="#">
<div class="carousel-caption">
1
</div>
</div>
<div class="item">
<img src="http://res.cloudinary.com/dyb29pfpm/image/upload/v1507608948/1507514383114_ksq1ib.jpg" alt="#">
<div class="carousel-caption">
2
</div>
</div>
<div class="item">
<img src="http://res.cloudinary.com/dyb29pfpm/image/upload/v1507608944/1507514233895_bv935a.jpg" alt="#">
<div class="carousel-caption">
2
</div>
</div>
</div>

<!-- Controls -->
<a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
</div>
<div class="col-sm-3">
</div>
</div>
</template>
<script>
export default {
}
</script>

这是一个轮播图,但是我们需要怎样才可以看到效果呢,怎样取代中间部分,这就涉及到了路由的问题了,vue-router可以登场了

在刚才的src目录下,还有一个router目录,查看里面的index.js文件,此时是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Hello',
component: HelloWorld
}
]
})

于是我们明白为什么,首页会显示helloworld效果,是因为路由指定读取了在路径为’’/‘’的时候读取该组件,于是我们将该部分换成我们自己的组件

此时的index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
}
]
})

修改后,我们可以查看效果

index2.png

现在的效果就比较优雅了,但是右边好像比较空虚,于是我们可以设置一个侧边栏,考虑到,不仅仅在首页要用到侧边栏,于是我们将侧边栏写成一个组件,方便多个页面调用该组件

2.3创建通用部分组件

Sidebar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="panel panel-default">
<div class="panel-heading">
热门课程
</div>
<img src="http://res.cloudinary.com/dyb29pfpm/image/upload/v1507608952/1507514293423_jv25xh.jpg" class="img-responsive" alt="Responsive image">
<img src="http://res.cloudinary.com/dyb29pfpm/image/upload/v1507608952/1507514293423_jv25xh.jpg" class="img-responsive" alt="Responsive image">
<img src="http://res.cloudinary.com/dyb29pfpm/image/upload/v1507608952/1507514293423_jv25xh.jpg" class="img-responsive" alt="Responsive image">
</div>
</template>

<script>
export default {

}
</script>

那我们该怎样在父组件Home.vue里面加入组件

template部分

1
2
3
4
5
6
7
<template>

...
<div class="col-sm-3">
<sidebar></sidebar>
</div>
</template>

script部分

1
2
3
4
5
6
7
8
<script>
import Sidebar from "./Sidebar.vue"
export default {
components:{
'sidebar': Sidebar,
},
}
</script>

此时由于热加载,直接就可以看到加入了侧边栏后的效果

于是一个完全没有数据交互的展示型首页就出现了,接下来我们要创建一些有其他功能的界面

2.4创建录入书籍页面

录入页面可以算是控制台了,为了管理方便,肯定是会有一个控制台页面进行集成,但此处我们只讲实现这一个功能的页面

创建BookAdmin.vue

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

<template>
<div class="panel panel-default">
<div class="panel-heading">
录入书籍
</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<label>书籍名称</label>
<input
type="text"
class="form-control"
v-model="book.name"
placeholder="name"
/>
</div>
<div class="col-sm-12">
<label>编写老师</label>
<input
type="text"
class="form-control"
v-model="book.teacher"
placeholder="teacher"
/>
</div>
<div class="col-sm-12">
<label>书籍简介</label>
<input
type="text"
class="form-control"
v-model="book.introduction"
placeholder="introduction"
/>
</div>
<div class="col-sm-12">
<label>购买链接</label>
<input
type="text"
class="form-control"
v-model="book.shopUrl"
placeholder="shopUrl"
/>
</div>
<div class="col-sm-12">
<label>图片链接</label>
<input
type="text"
class="form-control"
v-model="book.pictureUrl"
placeholder="pictureUrl"
/>
</div>
</div>
<button class="btn btn-primary" @click="save()">录入</button>
<router-link to="/" class="btn btn-danger">取消</router-link>
<hr>
</div>
</div>
</div>
</template>

<script>
export default {
name : 'BookAdmin',
data() {
return {
book:{
name : '',
teacher : '',
introduction : '',
shopUrl : '',
pictureUrl : '',
}
}
},
methods:{
save() {
this.$http.post('localhost:8080/createBook',{
name : this.book.name,
teacher : this.book.teacher,
shopUrl : this.book.shopUrl,
pictureUrl : this.book.pictureUrl,
introduction : this.book.introduction
}).then(function(ret){
console.log(ret);
let book = this.book
console.log(book);
location.href="/#/books"
})
}
}
}
</script>

为了能够反问到我们的页面,我们都知道该去添加新的路由了

index.js修改成如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import BookAdmin from '@/components/BookAdmin'

...

routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/bookAdmin',
name: 'BookAdmin',
component: BookAdmin
}
]

然后在浏览器访问http://localhost:8080/#/bookAdmin

此时的效果如下

bookadmin.png

那我们该怎样向后台post表单数据呢

大家可以看组件的script部分

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
<script>
export default {
name : 'BookAdmin',
data() {
return {
book:{
name : '',
teacher : '',
introduction : '',
shopUrl : '',
pictureUrl : '',
}
}
},
methods:{
save() {
this.$http.post('localhost:8080/createBook',{
name : this.book.name,
teacher : this.book.teacher,
shopUrl : this.book.shopUrl,
pictureUrl : this.book.pictureUrl,
introduction : this.book.introduction
}).then(function(ret){
console.log(ret);
let book = this.book
console.log(book);
location.href="/#/books"
})
}
}
}
</script>

首先使用了一个data()方法,返回的book对象,里面包含了我们需要接收关于一个book 的所有属性,此时为空,然后我再下面又引入了一个methods对象,里面包含了点击按钮触发的save()方法

1
this.$http.post('localhost:8080/createBook',{}).then(function(){})

便是vue-resource的使用方法,此处的url-‘localhost:8080/createBook’,便是发送的请求链接,于是大家便知道,我们接下来要定义后台的监听端口,并定义/createBook方法,才能使这个方法生效

2.5完成数据交互

首先安装需要的依赖

1
npm install express morgan mongodb body-parser --save-dev

Morgan和body-parser,分别用来log美化和解析参数。然后再根目录创建app.js作为入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var morgan = require('morgan');
var MongoClient = require('mongodb').MongoClient;
//确定数据库名称vuetest
var mongoUrl = 'mongodb://localhost:27017/vuetest';
var _db;
app.use(morgan('dev'));
app.use(bodyParser.json());
app.use(express.static('dist'));
MongoClient.connect(mongoUrl, function (err, db) {
if(err) {
console.error(err);
return;
}
console.log('mongodb have connected your project');
_db = db;
//监听端口8080
app.listen(8080, function () {
console.log('server is running at 8080');
});
});

通过上面的配置,我们创建了名为vuetest的数据库,确定了监听端口8080

接下来继续在app.js编写/createBook方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//增加书籍
app.post('/createBook', function(req, res, next) {
var request = req.body;
//接受请求
var collection = _db.collection('book');
//创建名为book的数据表
if(!request.name || !request.teacher || !request.introduction || !request.shopUrl || !request.pictureUrl) {
res.send({errcode:-1,errmsg:"参数不完整"});
return;
}
//插入数据表
collection.insert({name: request.name, teacher: request.teacher,introduction: request.introduction,shopUrl: request.shopUrl,pictureUrl: request.pictureUrl,}, function (err, ret) {
if(err) {
console.error(err);
res.status(500).end();
} else {
res.send({errcode:0,errmsg:"ok"});
}
});
});

接下来需要开启我们的mongodb服务,本地开发,就得在本机的windows或者mac上安装mongodb

完成了mongodb安装之后

切换到mongodb目录

1
$ ./mongod --dbpath E:\data

使用这个命令启动mongodb服务 dbpath是为了确定数据存储的路径,我的本地数据库存储路径就是E:\data

mongo.png

有上图的显示表示开启成功

接下来,打包构建并运行我们的项目

1
2
npm run build
node app.js

可以在localhost:8080访问我们的项目,直接http://localhost:8080/#/bookAdmin到达录入页

输入一些信息进行测试

test.png

在XHR请求里面可以看到一切正常,数据已经插入到数据库,并且此时已经跳转到了/books列表页了,由于没有写列表页,所以中间部分是空白的

2.6创建数据展示组建-列表页

BookList.vue

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<template>
<div>
<div class="col-sm-9">
<div>
<i class="glyphicon glyphicon-list-alt"></i>
书籍列表
</div>
<hr>
<div>
<p v-if="!books.length"><strong>还没有任何书籍</strong></p>

<div class="list-group">
<a class="list-group-item" v-for="(book,index) in books" :href="book.shopUrl">
<div class="row">
<div class="col-sm-3 book-avatar">
<img :src="book.pictureUrl" class="avatar img-responsive" />
</div>

<div class="col-sm-4 text-center">
<h5 class="list-group-item-text total-time">
<i class="glyphicon glyphicon-book"></i>
{{ book.name }}
</h5>
<p class="label label-primary text-center">
<i class="glyphicon glyphicon-user"></i>
{{ book.teacher }}
</p>
</div>

<div class="col-sm-5">
<p>{{ book.introduction }}</p>
</div>

</div>
</a>

</div>
</div>
</div>
<div class="col-sm-3">
<sidebar></sidebar>
</div>
</div>
</template>
<script>
import Sidebar from "./Sidebar.vue"
export default {
components:{
'sidebar': Sidebar,
},
name : 'BookList',
data(){
return {
books:[]
}
},
created(){
document.title="书籍列表"
this.$http.get('http://localhost:8080/book-list')
.then(function(ret) {
this.books = ret.data;
console.log(ret.data)
})
.then(function(err) {
console.log(err);
})
},
}
</script>
<style>
.avatar {
width:100%;
margin-top: 10px;
margin-bottom: 10px;
}
.book-avatar {
width:100%;
background-color: #f5f5f5;
border-top: 18px;
}
</style>

套路和之前一模一样

再到router的index.js插入路径

1
2
3
4
5
6
7
8
...
import BookList from '@/components/BookList'
...
...
{
path: '/books',
component: BookList,
},

然后回到app.js添加控制方法

1
2
3
4
5
6
7
8
9
10
11
12
//获取书籍列表
app.get('/book-list', function(req, res, next) {
var collection = _db.collection('book');
//使用了数据表查找全部的方法
collection.find({}).toArray(function (err, ret) {
if(err) {
console.error(err);
return;
}
res.json(ret);
});
});

然后我又去http://localhost:8080/#/bookAdmin录入了一本书,此时跳转之后的效果

book-list.png

感觉还是不错的

于是到此处,关于本地环境下的项目,已经跑通全栈,接下来,便是服务器跑通的环节

3.完成服务器环境下的项目开发

3.1首先是先将项目上传到服务器

在此处我选择使用git,然后考虑我们应该上传哪些到服务器,dist生成文件肯定是要的,然后是app.js涉及到的部分,不需要的在.gitgignore里面便可以再上传时被忽视

现在github上创建一个git仓库https://github.com/decadeheart/vue-example.git

在本地

1
2
3
4
5
git init 
git remote add origin https://github.com/decadeheart/vue-example.git
git add .
git commit -m "上传服务器"
git push origin master

这个时候可以切换到服务器了

至于服务的来源,推荐腾讯云,学生优惠之前可以抢1元一个月的,现在我买的是10元一个月,最低配置,但也可以拿来耍耍

1
2
ssh root@xxx.xx.xx.xx
//后面的是ip

登录上去之后,首先将项目文件拉取下来

1
2
3
4
5
6
7
8
cd /home
mkdir vue-exmaple
cd vue-exmaple
git init
//前提是你的服务器安装好了git
git remote add origin https://github.com/decadeheart/vue-example.git
git pull origin master
ls

接下来就是想要运行node app.js来运行项目了

但是肯定是需要依赖的,首先便就是服务器上的nodejs,还有服务器上的mongodb

3.2安装服务器上的依赖

顺序应该是

在我们启动了mongodb之后。可以考虑来安装运行app.js的依赖

1
2
3
npm i express body-parser morgan mongodb
//安装完成后尝试运行
node app.js

访问http://ip:8080/#/就可以在网页看到网上的效果

但是这个时候,我们所有的交互数据都出现了问题,报下面这个错误

error.png

这是因为我们发送的请求loacalhost并不是我们现在的服务器ip,所以连接的不是服务器数据库,被拒绝,需要修改成

1
2
http://119.29.208.124:8080/createBook
http://119.29.208.124:8080/book-list

然后重新打包,打包之后上传git再拉下来更新,就发现我们可以正常录入信息到数据库了,作为在服务器上运行的项目,肯定是希望他在服务器上永久运行,所以可以安装pm2或者forever等工具使得项目永久在服务器上运行

由于我的服务器原因,下载pm2老是失败,所以我选择了forever

1
2
npm i forever g
forever start app.js

常用命令如下

1
2
3
4
$ npm install forever -g   #安装
$ forever start app.js #启动
$ forever stop app.js #关闭
$ forever start -l forever.log -o out.log -e err.log app.js #输出日志和错误

然后你就可以推出当前目录去做其他事情了

讲道理,这个项目现在已经可以在服务器自主运行,终于完结撒花了

最后附上github地址

https://github.com/decadeheart/vue-example.git

打赏一瓶冰阔落,马上继续又创作