码农小志

码农小志 查看完整档案

北京编辑东北石油大学秦皇岛分校  |  软件工程 编辑新华集团  |  全栈开发工程师 编辑 jszhai.cn 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

码农小志 赞了回答 · 2020-01-13

解决puppeteer循环遍历cpu飙升99%导致服务器无响应假死

换成循环打开 page 效果如何?

关注 3 回答 2

码农小志 赞了回答 · 2020-01-13

elementUI的Table展开行以及row怎么获取?

啥意思啊,expand里面的table的column继续用slot-scope包着俩按钮不就妥了

关注 3 回答 2

码农小志 赞了回答 · 2019-12-26

解决django前后端完全分离如何做登录验证呢?

如果是 Django REST Framework, 则可以试试 JWT

关注 8 回答 6

码农小志 提出了问题 · 2019-08-09

elementUI的Table展开行以及row怎么获取?

是这样,在工作中用到了elementUI的Table组件,现在有表格A,表每一行暂且称为是A1 A2 A3...
要求的是点击一个表格A中某一行的查看按钮,会获取到的相关数据来展现.展现的数据是多条(这里称每一行为A1b1 A1b2 A1b3...),其中每一条数据都有相应的编辑和删除按钮,那么请问怎么样能在我点击A1b1 A1b2 A1b3...时候获取被点击的A1b1 A1b2 A1b3的row呢?
**现在我点击A1中A1b1 A1b2 A1b3获取到的都是A1的那一行的row.同样点击A2中获取的都是A2的那一行的row.
获取不到A1b1 A1b2 A1b3 B1b1 B1b2 B1b3...**

handleEditSpeArt方法就是获取被点击的数据,但是获取到的都是父级表格

如果看不懂就看这个图,这就是结构:
屏幕快照 2019-08-09 下午9.17.18
clipboard.png


代码如下:

<template>
    <div class="app-container">
        <el-collapse accordion v-model="activeName">
            <el-collapse-item name='1'>
                <template class="controller" slot="title">
                点此添加新的专题
                </template>
                <el-form ref="form" class="addInterested" :model="form" size="mini" fit label-width="80px" style="width:50%;">
                    <el-form-item label="专题名称">
                        <el-input v-model="form.title"></el-input>
                    </el-form-item>
                    
                    <el-form-item label="专题图标">
                        <el-upload
                            class="upload-demo"
                            action="/management/pictures"
                            :on-change="handleChange"
                            :auto-upload="true"
                            :http-request="handleUpload"
                            list-type="picture-card"
                            :limit=1
                            :file-list="fileList">
                            <el-button size="small" type="primary">点击上传</el-button>
                            <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
                        </el-upload>
                    </el-form-item>

                    <!-- <el-form-item label="ico背景色">
                        <el-color-picker color-format="hex" @click="changColor" v-model="form.color" size="small"></el-color-picker>
                </el-form-item>  -->

                    <el-form-item label="专题描述">
                        <el-input type="textarea" v-model="form.description"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" @click="onSubmit">确认</el-button>
                        <el-button @click="onReset">重置</el-button>
                    </el-form-item>
                    
                </el-form>

          </el-collapse-item>
</el-collapse>

        <el-table 
        ref="table"
        :data="specialList" 
        stripe
        style="width: 100%"
        
        >
            <!-- <el-table-column label="标识" width="100">
                <template slot-scope="scope">
                <span style="margin-left: 0px">{{ scope.row.sub_id }}</span>
                </template>
            </el-table-column> -->

            <el-table-column label="专题名" width="300">
                <template slot-scope="scope">
                <span style="margin-left: 0px">{{ scope.row.title }}</span>
                </template>
            </el-table-column>

            <el-table-column label="图标" width="120">
                <template slot-scope="scope">
                    <img :data-original="scope.row.cover_url" alt="" class="icon_url">
                </template>
            </el-table-column>
            <el-table-column label="文章数" width="">
                <template slot-scope="scope">
                <span style="margin-left: 0px">{{ scope.row.news_count }}篇</span>
                </template>
            </el-table-column>
            <el-table-column label="专题说明" width="">
                <template slot-scope="scope">
                <span style="margin-left: 0px">{{ scope.row.description }}</span>
                </template>
            </el-table-column>

            <el-table-column label="操作" width="400">
                <template slot-scope="scope">
                    <el-button
                    size="mini"
                    @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
                    <el-button
                    size="mini"
                    @click="handleAdd(scope.$index, scope.row)">发布</el-button>
                    <el-button
                    size="mini"
                    @click="getSpecialListArticles(scope.row)">查看文章</el-button>
                    <el-button
                    size="mini"
                    type="danger"
                    @click="handleDelete(scope.$index, scope.row)">删除</el-button>
                </template>
            </el-table-column>
            


            <!-- 展开行 -->

            <el-table-column type="expand" width="1">
                <template slot-scope="props">
                    <!-- 这里显示文章列表 -->
                    <el-table
                        empty-text="当前专题暂无文章"
                         v-loading="specialListArticlesLoading"
                        :data="specialListArticles"
                        height="250"
                        border
                        style="width: 100%">
                            <el-table-column
                            prop="news_id"
                            label="文章编号"
                            >
                            </el-table-column>
                            <el-table-column
                            prop="title"
                            label="文章标题"
                            >
                            </el-table-column>
                            <el-table-column
                            prop="news_type"
                            label="文章类型"
                            >
                            </el-table-column>
                            <el-table-column
                            prop="post_time"
                            label="发布日期"
                            >
                            </el-table-column>
                            <el-table-column
                            label="操作"
                            >
                                <el-button
                                size="mini"
                                @click="handleEditSpeArt(props.row)">编辑</el-button>

                                <el-button
                                size="mini"
                                type="danger"
                                @click="handleDeleteSpeArt(props.$index, props.row)">删除</el-button>

                            </el-table-column>

                            <!-- <el-table-column
                            prop="post_time"
                            label="新闻源">
                            </el-table-column>
                            <el-table-column
                            prop="post_time"
                            label="媒体类型">
                            </el-table-column>
                            <el-table-column
                            prop="post_time"
                            label="操作">
                            </el-table-column> -->
                    </el-table>

                </template>
            </el-table-column>
        </el-table>

        <div class="block pagerr">
            <el-pagination
            background
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            :current-page="currentPage4"
            :page-sizes="[10, 50, 100, 200]"
            :page-size="pageSize"
            layout="total, prev, pager, next, jumper"
            :total="total">
            </el-pagination>
        </div>


    </div>
</template>

<script>

import { uploadImage } from '@/api/article'//
import { getSpecialList ,newSpecial,updateSpecial,deleteSpecial,getSpecialArticlesFnc } from '@/api/special'//
import { constants } from 'crypto';
import { setTimeout } from 'timers';
export default {
    name: 'interested',
    data(){
        return {
            specialList: [],
            form: {
                title: ``,
                cover_url: ``,
                description: ``,
                sub_id: null,
                
            },
            fileList: [
                // {
                // name: 'food.jpeg',
                // url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
                // status: 'finished'
                // }
                ],
            currentPage4: 1,
            total: 0,
            pageSize: 10,
            pageIndex: 1,
            
            activeName: '',
            specialListArticles: [],//专题下文章列表
            specialListArticlesLoading: true
        }
    },
    methods: {
        //点击编辑
        handleEdit(index, row) {
            console.log( row );
            this.activeName = `1`;
            this.form = {
                title: `${row.title}`,
                cover_url: `${row.cover_url}`,
                description: `${row.description}`,
                id: `${row.sub_id}`
            }
            this.fileList = [{url: `${row.cover_url}`}]

        },
        //在该专题下发布新文章
        handleAdd(index,row){
            console.log( index, row );
            let sub_id = row.sub_id;
            this.$router.push(`/articles/newarticle?sub_id=${sub_id}`);
        },
        handleDelete(index, row) {
           deleteSpecial({
               id: row.sub_id
           }).then(e=>{
               if( e.status_code == 200 ){
                    this.$message({
                        message: `删除成功`,
                        type: "success",
                        duration: 5000
                    });
                    this.form = {
                        title: ``,
                        cover_url: ``,
                        description: ``,
                        id: null
                    }
                    this.fileList = [];
                    this.getData();
               }
           });
        },
    
        onSubmit() {
            console.log(this.form);
            if( !this.form.title || !this.form.cover_url ||! this.form.description ){
                this.$message.error('专题名称图标以及专题描述均需要填写');
                return false;   
            }
        if( this.form.id ){
            //存在id  走更新分支
            updateSpecial({
                title: this.form.title,
                cover_url: this.form.cover_url,
                description: this.form.description,
                id: this.form.id
            }).then(e=>{
                if(e && e.status_code == 200){
                    this.$message({
                        message: `添加成功`,
                        type: "success",
                        duration: 5000
                    });
                    this.form = {
                        title: ``,
                        cover_url: ``,
                        description: ``,
                        id: null
                    }
                    this.fileList = [];
                    this.getData();
                }else{
                    this.$message({
                        message: `添加失败`,
                        type: "warning",
                        duration: 5000
                    });
                };
            })
            return false;
        }

            newSpecial({
                title: this.form.title,
                cover_url: this.form.cover_url,
                description: this.form.description,
                id: this.form.id
            }).then(e=>{
                if(e && e.status_code == 200){
                    this.$message({
                        message: `添加成功`,
                        type: "success",
                        duration: 5000
                    });
                    this.form = {
                        title: ``,
                        cover_url: ``,
                        description: ``,
                        id: null
                    }
                    this.fileList = [];
                    this.getData();
                }else{
                    this.$message({
                        message: `添加失败`,
                        type: "warning",
                        duration: 5000
                    });
                };
            })
            // .catch(err=>{
            //     console.log(err);
            //     if( JSON.stringify(err).includes('400') ){
            //         this.$message({
            //             message: `已经存在`,
            //             type: "warning",
            //             duration: 5000
            //         });
            //     }else if( JSON.stringify(err).includes('405') ){
            //         this.$message({
            //             message: `PUT方法不被允许,如要修改此兴趣请删除后重新添加`,
            //             type: "warning",
            //             duration: 5000
            //         });
            //     }
            // });
        },
        onReset(){
            console.log('onReset!');
            this.form = {
                title: ``,
                cover_url: ``,
                description: ``,
                id: null
            }

            this.fileList = [];
        },

        //上传图片相关
        handleChange(file, fileList) {
            console.log(file,fileList);
            
        },

        handleUpload(item){
            let formData = new FormData();
            formData.append("file", item.file);
            uploadImage(formData).then( result =>{
                console.log("上传结果", result);
                this.form.cover_url = result.data.url_name;
            });
        },

         handleSizeChange(val) {
            console.log(`每页 ${val} 条`);
            this.pageSize = val;
            this.created;
        },
        handleCurrentChange(val) {
            console.log(`当前页: ${val}`);
            this.pageIndex = val;
            this.getData();
        },
        getData(){
            getSpecialList(this.pageSize,this.pageIndex).then(e=>{
                console.log(e);
                this.specialList = e.data;
                this.total = e.count?e.count:0;
            });
        },

        changColor(e){
            console.log(e);
        },

        //获取专题下文章列表

        getSpecialListArticles(row){
            this.specialListArticlesLoading = true;
            this.specialListArticles = [];

            let $table = this.$refs.table;
            // console.log(this.specialList );
            this.specialList.map(e=>{
                // console.log( row,e );
                if (Number( row.sub_id )!= Number( e.sub_id )) {
                   $table.toggleRowExpansion(e, false)
                 }
            });
            $table.toggleRowExpansion(row);

            setTimeout(()=>{
                //获取相应数据
                getSpecialArticlesFnc(row.sub_id).then(e=>{
                    this.specialListArticles = e.data?e.data:[];
                }).then(()=>{
                    this.specialListArticlesLoading = false;
                });
            },500);
            
            
            
        },

        handleEditSpeArt( id ){
            console.log( id );
            // let id = row.



        }






    },
    created(){
        this.getData();
        
    }
}
</script>

<style scoped>
.icon_url{
    width: 50px;
}
.addInterested{
    margin-left: auto;
    margin-right: auto;
    border: 1px solid lightblue;
    padding: 50px;
    margin-bottom: 50px;
}

/* 上传图片 */


.avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
  .avatar-uploader .el-upload:hover {
    border-color: #409EFF;
  }
  .avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    line-height: 178px;
    text-align: center;
  }
  .avatar {
    width: 178px;
    height: 178px;
    display: block;
  }

.pagerr{
    padding: 30px 24px;
    border-bottom: 1px solid #eff2f6;
}

.controller{
    text-align: center;
    margin-left: auto;
    margin-right: auto;
    background-color: #409EFF;
}
  
</style>

如果代码不够全面我继续贴,官方文档我看了,没找到这类解决办法.

这水平线下面是动图我不知道会不会动,先放上来看看

clipboard.png

关注 3 回答 2

码农小志 收藏了文章 · 2019-07-31

带你五步学会Vue SSR

前言

SSR大家肯定都不陌生,通过服务端渲染,可以优化SEO抓取,提升首页加载速度等,我在学习SSR的时候,看过很多文章,有些对我有很大的启发作用,有些就只是照搬官网文档。通过几天的学习,我对SSR有了一些了解,也从头开始完整的配置出了SSR的开发环境,所以想通过这篇文章,总结一些经验,同时希望能够对学习SSR的朋友起到一点帮助。

我会通过五个步骤,一步步带你完成SSR的配置:

  1. 纯浏览器渲染
  2. 服务端渲染,不包含Ajax初始化数据
  3. 服务端渲染,包含Ajax初始化数据
  4. 服务端渲染,使用serverBundleclientManifest进行优化
  5. 一个完整的基于Vue + VueRouter + Vuex的SSR工程

如果你现在对于我上面说的还不太了解,没有关系,跟着我一步步向下走,最终你也可以独立配置一个SSR开发项目,所有源码我会放到github上,大家可以作为参考

正文

1. 纯浏览器渲染

这个配置相信大家都会,就是基于weback + vue的一个常规开发配置,这里我会放一些关键代码,完整代码可以去github查看。

目录结构
- node_modules
- components  
    - Bar.vue
    - Foo.vue
- App.vue
- app.js
- index.html
- webpack.config.js
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore
app.js
import Vue from 'vue';
import App from './App.vue';

let app = new Vue({
  el: '#app',
  render: h => h(App)
});
App.vue
<template>
  <div>
    <Foo></Foo>
    <Bar></Bar>
  </div>
</template>

<script>
import Foo from './components/Foo.vue';
import Bar from './components/Bar.vue';

export default {
  components: {
    Foo, Bar
  }
}
</script>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>纯浏览器渲染</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>
components/Foo.vue
<template>
  <div class="foo">
    <h1>Foo Component</h1>
  </div>
</template>

<style>
.foo {
  background: yellowgreen;
}
</style>
components/Bar.vue
<template>
  <div class="bar">
    <h1>Bar Component</h1>
  </div>
</template>

<style>
.bar {
  background: bisque;
}
</style>
webpack.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  mode: 'development',

  entry: './app.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader', 'postcss-loader']
        // 如果需要单独抽出CSS文件,用下面这个配置
        // use: ExtractTextPlugin.extract({
        //   fallback: 'vue-style-loader',
        //   use: [
        //     'css-loader',
        //     'postcss-loader'
        //   ]
        // })
      },
      {
        test: /\.(jpg|jpeg|png|gif|svg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10000    // 10Kb
          }
        }
      },
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  },

  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './index.html'
    }),
    // 如果需要单独抽出CSS文件,用下面这个配置
    // new ExtractTextPlugin("styles.css")
  ]
};
postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};
.babelrc
{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    // 让其支持动态路由的写法 const Foo = () => import('../components/Foo.vue')
    "dynamic-import-webpack"    
  ]
}
package.json
{
  "name": "01",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "yarn run dev",
    "dev": "webpack-dev-server",
    "build": "webpack"
  },
  "dependencies": {
    "vue": "^2.5.17"
  },
  "devDependencies": {
    "@babel/core": "^7.1.2",
    "@babel/preset-env": "^7.1.0",
    "babel-plugin-dynamic-import-webpack": "^1.1.0",
    "autoprefixer": "^9.1.5",
    "babel-loader": "^8.0.4",
    "css-loader": "^1.0.0",
    "extract-text-webpack-plugin": "^4.0.0-beta.0",
    "file-loader": "^2.0.0",
    "html-webpack-plugin": "^3.2.0",
    "postcss": "^7.0.5",
    "postcss-loader": "^3.0.0",
    "url-loader": "^1.1.1",
    "vue-loader": "^15.4.2",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.5.17",
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.9"
  }
}
命令
启动开发环境
yarn start
构建生产环境
yarn run build

最终效果截图:

纯浏览器渲染

完整代码查看github

2. 服务端渲染,不包含Ajax初始化数据

服务端渲染SSR,类似于同构,最终要让一份代码既可以在服务端运行,也可以在客户端运行。如果说在SSR的过程中出现问题,还可以回滚到纯浏览器渲染,保证用户正常看到页面。

那么,顺着这个思路,肯定就会有两个webpack的入口文件,一个用于浏览器端渲染weboack.client.config.js,一个用于服务端渲染webpack.server.config.js,将它们的公有部分抽出来作为webpack.base.cofig.js,后续通过webpack-merge进行合并。同时,也要有一个server来提供http服务,我这里用的是koa

我们来看一下新的目录结构:

- node_modules
- config    // 新增
    - webpack.base.config.js
    - webpack.client.config.js
    - webpack.server.config.js
- src
    - components  
        - Bar.vue
        - Foo.vue
    - App.vue
    - app.js
    - entry-client.js   // 新增
    - entry-server.js   // 新增
    - index.html
    - index.ssr.html    // 新增
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore

在纯客户端应用程序(client-only app)中,每个用户会在他们各自的浏览器中使用新的应用程序实例。对于服务器端渲染,我们也希望如此:每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染(cross-request state pollution)。

所以,我们要对app.js做修改,将其包装为一个工厂函数,每次调用都会生成一个全新的根组件。

app.js

import Vue from 'vue';
import App from './App.vue';

export function createApp() {
  const app = new Vue({
    render: h => h(App)
  });

  return { app };
}

在浏览器端,我们直接新建一个根组件,然后将其挂载就可以了。

entry-client.js

import { createApp } from './app.js';

const { app } = createApp();

app.$mount('#app');

在服务器端,我们就要返回一个函数,该函数的作用是接收一个context参数,同时每次都返回一个新的根组件。这个context在这里我们还不会用到,后续的步骤会用到它。

entry-server.js

import { createApp } from './app.js';

export default context => {
  const { app } = createApp();

  return app;
}

然后再来看一下index.ssr.html

index.ssr.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>服务端渲染</title>
</head>
<body>
  <!--vue-ssr-outlet-->

  <script type="text/javascript" data-original="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
</html>

<!--vue-ssr-outlet-->的作用是作为一个占位符,后续通过vue-server-renderer插件,将服务器解析出的组件html字符串插入到这里。

<script type="text/javascript" data-original="<%= htmlWebpackPlugin.options.files.js %>"></script>是为了将webpack通过webpack.client.config.js打包出的文件放到这里(这里是为了简单演示,后续会有别的办法来做这个事情)。

因为服务端吐出来的就是一个html字符串,后续的Vue相关的响应式、事件响应等等,都需要浏览器端来接管,所以就需要将为浏览器端渲染打包的文件在这里引入。

用官方的词来说,叫客户端激活(client-side hydration)

所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。

在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:

// 这里假定 App.vue template 根元素的 `id="app"`
app.$mount('#app')

由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。相反,我们需要"激活"这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)。

如果你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:

<div id="app" data-server-rendered="true">

Vue在浏览器端就依靠这个属性将服务器吐出来的html进行激活,我们一会自己构建一下就可以看到了。

接下来我们看一下webpack相关的配置:

webpack.base.config.js

const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  mode: 'development',

  resolve: {
    extensions: ['.js', '.vue']
  },

  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].bundle.js'
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader', 'postcss-loader']
      },
      {
        test: /\.(jpg|jpeg|png|gif|svg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10000    // 10Kb
          }
        }
      }
    ]
  },

  plugins: [
    new VueLoaderPlugin()
  ]
};

webpack.client.config.js

const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');

module.exports = merge(base, {
  entry: {
    client: path.resolve(__dirname, '../src/entry-client.js')
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/index.html'),
      filename: 'index.html'
    })
  ]
});

注意,这里的入口文件变成了entry-client.js,将其打包出的client.bundle.js插入到index.html中。

webpack.server.config.js

const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');

module.exports = merge(base, {
  target: 'node',
  entry: {
    server: path.resolve(__dirname, '../src/entry-server.js')
  },
  output: {
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/index.ssr.html'),
      filename: 'index.ssr.html',
      files: {
        js: 'client.bundle.js'
      },
      excludeChunks: ['server']
    })
  ]
});

这里有几个点需要注意一下:

  1. 入口文件是 entry-server.js
  2. 因为是打包服务器端依赖的代码,所以target要设为node,同时,outputlibraryTarget要设为commonjs2

这里关于HtmlWebpackPlugin配置的意思是,不要在index.ssr.html中引入打包出的server.bundle.js,要引为浏览器打包的client.bundle.js,原因前面说过了,是为了让Vue可以将服务器吐出来的html进行激活,从而接管后续响应。

那么打包出的server.bundle.js在哪用呢?接着往下看就知道啦~~

package.json

{
  "name": "01",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "yarn run dev",
    "dev": "webpack-dev-server",
    "build:client": "webpack --config config/webpack.client.config.js",
    "build:server": "webpack --config config/webpack.server.config.js"
  },
  "dependencies": {
    "koa": "^2.5.3",
    "koa-router": "^7.4.0",
    "koa-static": "^5.0.0",
    "vue": "^2.5.17",
    "vue-server-renderer": "^2.5.17"
  },
  "devDependencies": {
    "@babel/core": "^7.1.2",
    "@babel/preset-env": "^7.1.0",
    "autoprefixer": "^9.1.5",
    "babel-loader": "^8.0.4",
    "css-loader": "^1.0.0",
    "extract-text-webpack-plugin": "^4.0.0-beta.0",
    "file-loader": "^2.0.0",
    "html-webpack-plugin": "^3.2.0",
    "postcss": "^7.0.5",
    "postcss-loader": "^3.0.0",
    "style-loader": "^0.23.0",
    "url-loader": "^1.1.1",
    "vue-loader": "^15.4.2",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.5.17",
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.9",
    "webpack-merge": "^4.1.4"
  }
}

接下来我们看server端关于http服务的代码:

server/server.js

const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const path = require('path');
const fs = require('fs');
const backendApp = new Koa();
const frontendApp = new Koa();
const backendRouter = new Router();
const frontendRouter = new Router();

const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
  template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')
});

// 后端Server
backendRouter.get('/index', (ctx, next) => {
  // 这里用 renderToString 的 promise 返回的 html 有问题,没有样式
  renderer.renderToString((err, html) => {
    if (err) {
      console.error(err);
      ctx.status = 500;
      ctx.body = '服务器内部错误';
    } else {
      console.log(html);
      ctx.status = 200;
      ctx.body = html;
    }
  });
});

backendApp.use(serve(path.resolve(__dirname, '../dist')));

backendApp
  .use(backendRouter.routes())
  .use(backendRouter.allowedMethods());

backendApp.listen(3000, () => {
  console.log('服务器端渲染地址: http://localhost:3000');
});


// 前端Server
frontendRouter.get('/index', (ctx, next) => {
  let html = fs.readFileSync(path.resolve(__dirname, '../dist/index.html'), 'utf-8');
  ctx.type = 'html';
  ctx.status = 200;
  ctx.body = html;
});

frontendApp.use(serve(path.resolve(__dirname, '../dist')));

frontendApp
  .use(frontendRouter.routes())
  .use(frontendRouter.allowedMethods());

frontendApp.listen(3001, () => {
  console.log('浏览器端渲染地址: http://localhost:3001');
});

这里对两个端口进行监听,3000端口是服务端渲染,3001端口是直接输出index.html,然后会在浏览器端走Vue的那一套,主要是为了和服务端渲染做对比使用。

这里的关键代码是如何在服务端去输出html`字符串。

const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.bundle.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
  template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')
});

可以看到,server.bundle.js在这里被使用啦,因为它的入口是一个函数,接收context作为参数(非必传),输出一个根组件app

这里我们用到了vue-server-renderer插件,它有两个方法可以做渲染,一个是createRenderer,另一个是createBundleRenderer

const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer({ /* 选项 */ })
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer(serverBundle, { /* 选项 */ })

createRenderer无法接收为服务端打包出的server.bundle.js文件,所以这里只能用createBundleRenderer

serverBundle 参数可以是以下之一:

  • 绝对路径,指向一个已经构建好的 bundle 文件(.js.json)。必须以 / 开头才会被识别为文件路径。
  • webpack + vue-server-renderer/server-plugin 生成的 bundle 对象。
  • JavaScript 代码字符串(不推荐)。

这里我们引入的是.js文件,后续会介绍如何使用.json文件以及有什么好处。

renderer.renderToString((err, html) => {
    if (err) {
      console.error(err);
      ctx.status = 500;
      ctx.body = '服务器内部错误';
    } else {
      console.log(html);
      ctx.status = 200;
      ctx.body = html;
    }
});

使用createRenderercreateBundleRenderer返回的renderer函数包含两个方法renderToStringrenderToStream,我们这里用的是renderToString成功后直接返回一个完整的字符串,renderToStream返回的是一个Node流。

renderToString支持Promise,但是我在使用Prmoise形式的时候样式会渲染不出来,暂时还不知道原因,如果大家知道的话可以给我留言啊。

配置基本就完成了,来看一下如何运行。

yarn run build:client       // 打包浏览器端需要bundle
yarn run build:server       // 打包SSR需要bundle

yarn start      // 其实就是 node server/server.js,提供http服务

最终效果展示:

访问http://localhost:3000/index

SSR没有ajax数据

我们看到了前面提过的data-server-rendered="true"属性,同时会加载client.bundle.js文件,为了让Vue在浏览器端做后续接管。

访问http://localhost:3001/index还和第一步实现的效果一样,纯浏览器渲染,这里就不放截图了。

完整代码查看github

3. 服务端渲染,包含Ajax初始化数据

如果SSR需要初始化一些异步数据,那么流程就会变得复杂一些。

我们先提出几个问题:

  1. 服务端拿异步数据的步骤在哪做?
  2. 如何确定哪些组件需要获取异步数据?
  3. 获取到异步数据之后要如何塞回到组件内?

带着问题我们向下走,希望看完这篇文章的时候上面的问题你都找到了答案。

服务器端渲染和浏览器端渲染组件经过的生命周期是有区别的,在服务器端,只会经历beforeCreatecreated两个生命周期。因为SSR服务器直接吐出html字符串就好了,不会渲染DOM结构,所以不存在beforeMountmounted的,也不会对其进行更新,所以也就不存在beforeUpdateupdated等。

我们先来想一下,在纯浏览器渲染的Vue项目中,我们是怎么获取异步数据并渲染到组件中的?一般是在created或者mounted生命周期里发起异步请求,然后在成功回调里执行this.data = xxxVue监听到数据发生改变,走后面的Dom Diff,打patch,做DOM更新。

那么服务端渲染可不可以也这么做呢?答案是不行的

  1. mounted里肯定不行,因为SSR都没有mounted生命周期,所以在这里肯定不行。
  2. beforeCreate里发起异步请求是否可以呢,也是不行的。因为请求是异步的,可能还没有等接口返回,服务端就已经把html字符串拼接出来了。

所以,参考一下官方文档,我们可以得到以下思路:

  1. 在渲染前,要预先获取所有需要的异步数据,然后存到Vuexstore中。
  2. 在后端渲染时,通过Vuex将获取到的数据注入到相应组件中。
  3. store中的数据设置到window.__INITIAL_STATE__属性中。
  4. 在浏览器环境中,通过Vuexwindow.__INITIAL_STATE__里面的数据注入到相应组件中。

正常情况下,通过这几个步骤,服务端吐出来的html字符串相应组件的数据都是最新的,所以第4步并不会引起DOM更新,但如果出了某些问题,吐出来的html字符串没有相应数据,Vue也可以在浏览器端通过`Vuex注入数据,进行DOM更新。

更新后的目录结构:

- node_modules
- config
   - webpack.base.config.js
   - webpack.client.config.js
   - webpack.server.config.js
- src
   - components  
       - Bar.vue
       - Foo.vue
   - store             // 新增
       store.js
   - App.vue
   - app.js
   - entry-client.js
   - entry-server.js   
   - index.html
   - index.ssr.html
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore

先来看一下store.js:

store/store.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const fetchBar = function() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('bar 组件返回 ajax 数据');
    }, 1000);
  });
};

function createStore() {
  const store = new Vuex.Store({
    state: {
      bar: ''
    },

    mutations: {
      'SET_BAR'(state, data) {
        state.bar = data;
      }
    },

    actions: {
      fetchBar({ commit }) {
        return fetchBar().then((data) => {
          commit('SET_BAR', data);
        }).catch((err) => {
          console.error(err);
        })
      }
    }
  });

  if (typeof window !== 'undefined' && window.__INITIAL_STATE__) {
    console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
    store.replaceState(window.__INITIAL_STATE__);
  }
  
  return store;
}

export default createStore;

typeof window

如果不太了解Vuex,可以去Vuex官网先看一些基本概念。

vuex

这里fetchBar可以看成是一个异步请求,这里用setTimeout模拟。在成功回调中commit相应的mutation进行状态修改。

这里有一段关键代码:

if (typeof window !== 'undefined' && window.__INITIAL_STATE__) {
    console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
    store.replaceState(window.__INITIAL_STATE__);
}

因为store.js同样也会被打包到服务器运行的server.bundle.js中,所以运行环境不一定是浏览器,这里需要对window做判断,防止报错,同时如果有window.__INITIAL_STATE__属性,说明服务器已经把所有初始化需要的异步数据都获取完成了,要对store中的状态做一个替换,保证统一。

components/Bar.vue

<template>
  <div class="bar">
    <h1 @click="onHandleClick">Bar Component</h1>
    <h2>异步Ajax数据:</h2>
    <span>{{ msg }}</span>
  </div>
</template>

<script>
  const fetchInitialData = ({ store }) => {
    store.dispatch('fetchBar');
  };

  export default {
    asyncData: fetchInitialData,

    methods: {
      onHandleClick() {
        alert('bar');
      }
    },

    mounted() {
      // 因为服务端渲染只有 beforeCreate 和 created 两个生命周期,不会走这里
      // 所以把调用 Ajax 初始化数据也写在这里,是为了供单独浏览器渲染使用
      let store = this.$store;
      fetchInitialData({ store });
    },

    computed: {
      msg() {
        return this.$store.state.bar;
      }
    }
  }
</script>

<style>
.bar {
  background: bisque;
}
</style>

这里在Bar组件的默认导出对象中增加了一个方法asyncData,在该方法中会dispatch相应的action,进行异步数据获取。

需要注意的是,我在mounted中也写了获取数据的代码,这是为什么呢? 因为想要做到同构,代码单独在浏览器端运行,也应该是没有问题的,又由于服务器没有mounted生命周期,所以我写在这里就可以解决单独在浏览器环境使用也可以发起同样的异步请求去初始化数据。

components/Foo.vue

<template>
  <div class="foo">
    <h1 @click="onHandleClick">Foo Component</h1>
  </div>
</template>

<script>
export default {
  methods: {
    onHandleClick() {
      alert('foo');
    }
  },
}
</script>

<style>
.foo {
  background: yellowgreen;
}
</style>

这里我对两个组件都添加了一个点击事件,为的是证明在服务器吐出首页html后,后续的步骤都会被浏览器端的Vue接管,可以正常执行后面的操作。

app.js

import Vue from 'vue';
import createStore from './store/store.js';
import App from './App.vue';

export function createApp() {
  const store = createStore();

  const app = new Vue({
    store,
    render: h => h(App)
  });

  return { app, store, App };
}

在建立根组件的时候,要把Vuex的store传进去,同时要返回,后续会用到。

最后来看一下entry-server.js,关键步骤在这里:

entry-server.js

import { createApp } from './app.js';

export default context => {
  return new Promise((resolve, reject) => {
    const { app, store, App } = createApp();

    let components = App.components;
    let asyncDataPromiseFns = [];
  
    Object.values(components).forEach(component => {
      if (component.asyncData) {
        asyncDataPromiseFns.push(component.asyncData({ store }));
      }
    });
  
    Promise.all(asyncDataPromiseFns).then((result) => {
      // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中
      context.state = store.state;
  
      console.log(222);
      console.log(store.state);
      console.log(context.state);
      console.log(context);
  
      resolve(app);
    }, reject);
  });
}

我们通过导出的App拿到了所有它下面的components,然后遍历,找出哪些componentasyncData方法,有的话调用并传入store,该方法会返回一个Promise,我们使用Promise.all等所有的异步方法都成功返回,才resolve(app)

context.state = store.state作用是,当使用createBundleRenderer时,如果设置了template选项,那么会把context.state的值作为window.__INITIAL_STATE__自动插入到模板html中。

这里需要大家多思考一下,弄清楚整个服务端渲染的逻辑。

如何运行:

yarn run build:client
yarn run build:server

yarn start

最终效果截图:

服务端渲染:打开http://localhost:3000/index

server-remder-ajax

可以看到window.__INITIAL_STATE__被自动插入了。

我们来对比一下SSR到底对加载性能有什么影响吧。

服务端渲染时performance截图

sercer_render_ajax_performance

纯浏览器端渲染时performance截图

client_render_ajax_performance

同样都是在fast 3G网络模式下,纯浏览器端渲染首屏加载花费时间2.9s,因为client.js加载就花费了2.27s,因为没有client.js就没有Vue,也就没有后面的东西了。

服务端渲染首屏时间花费0.8s,虽然client.js加载扔花费2.27s,但是首屏已经不需要它了,它是为了让Vue在浏览器端进行后续接管。

从这我们可以真正的看到,服务端渲染对于提升首屏的响应速度是很有作用的。

当然有的同学可能会问,在服务端渲染获取初始ajax数据时,我们还延时了1s,在这个时间用户也是看不到页面的。没错,接口的时间我们无法避免,就算是纯浏览器渲染,首页该调接口还是得调,如果接口响应慢,那么纯浏览器渲染看到完整页面的时间会更慢。

完整代码查看github

4. 使用serverBundle和clientManifest进行优化

前面我们创建服务端renderer的方法是:

const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
  template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')
});

serverBundle我们用的是打包出的server.bundle.js文件。这样做的话,在每次编辑过应用程序源代码之后,都必须停止并重启服务。这在开发过程中会影响开发效率。此外,Node.js 本身不支持 source map。

vue-server-renderer 提供一个名为 createBundleRenderer 的 API,用于处理此问题,通过使用 webpack 的自定义插件,server bundle 将生成为可传递到 bundle renderer 的特殊 JSON 文件。所创建的 bundle renderer,用法和普通 renderer 相同,但是 bundle renderer 提供以下优点:

  • 内置的 source map 支持(在 webpack 配置中使用 devtool: 'source-map'
  • 在开发环境甚至部署过程中热重载(通过读取更新后的 bundle,然后重新创建 renderer 实例)
  • 关键 CSS(critical CSS) 注入(在使用 *.vue 文件时):自动内联在渲染过程中用到的组件所需的CSS。更多细节请查看 CSS 章节。
  • 使用 clientManifest 进行资源注入:自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk

preloadprefetch有不了解的话可以自行查一下它们的作用哈。

那么我们来修改webpack配置:

webpack.client.config.js

const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const base = require('./webpack.base.config');

module.exports = merge(base, {
  entry: {
    client: path.resolve(__dirname, '../src/entry-client.js')
  },

  plugins: [
    new VueSSRClientPlugin(),   // 新增
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/index.html'),
      filename: 'index.html'
    })
  ]
});

webpack.server.config.js

const path = require('path');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const base = require('./webpack.base.config');

module.exports = merge(base, {
  target: 'node',
   // 对 bundle renderer 提供 source map 支持
  devtool: '#source-map',
  entry: {
    server: path.resolve(__dirname, '../src/entry-server.js')
  },
  externals: [nodeExternals()],     // 新增
  output: {
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new VueSSRServerPlugin(),   // 这个要放到第一个写,否则 CopyWebpackPlugin 不起作用,原因还没查清楚
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/index.ssr.html'),
      filename: 'index.ssr.html',
      files: {
        js: 'client.bundle.js'
      },
      excludeChunks: ['server']
    })
  ]
});

因为是服务端引用模块,所以不需要打包node_modules中的依赖,直接在代码中require引用就好,所以配置externals: [nodeExternals()]

两个配置文件会分别生成vue-ssr-client-manifest.jsonvue-ssr-server-bundle.json。作为createBundleRenderer的参数。

来看server.js

server.js

const serverBundle = require(path.resolve(__dirname, '../dist/vue-ssr-server-bundle.json'));
const clientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json'));
const template = fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8');

const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template: template,
  clientManifest: clientManifest
});

效果和第三步就是一样的啦,就不截图了,完整代码查看github

5. 配置一个完整的基于Vue + VueRouter + Vuex的SSR

这里和第四步不一样的是引入了vue-router,更接近于实际开发项目。

src下新增router目录。

router/index.js

import Vue from 'vue';
import Router from 'vue-router';
import Bar from '../components/Bar.vue';

Vue.use(Router);

function createRouter() {
  const routes = [
    {
      path: '/bar',
      component: Bar
    },
    {
      path: '/foo',
      component: () => import('../components/Foo.vue')   // 异步路由
    }
  ];

  const router = new Router({
    mode: 'history',
    routes
  });

  return router;
}

export default createRouter;

这里我们把Foo组件作为一个异步组件引入,做成按需加载。

app.js中引入router,并导出:

app.js

import Vue from 'vue';
import createStore from './store/store.js';
import createRouter from './router';
import App from './App.vue';

export function createApp() {
  const store = createStore();
  const router = createRouter();

  const app = new Vue({
    router,
    store,
    render: h => h(App)
  });

  return { app, store, router, App };
}

修改App.vue引入路由组件:

App.vue

<template>
  <div id="app">
    <router-link to="/bar">Goto Bar</router-link> 
    <router-link to="/foo">Goto Foo</router-link> 
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  beforeCreate() {
    console.log('App.vue beforeCreate');
  },

  created() {
    console.log('App.vue created');
  },

  beforeMount() {
    console.log('App.vue beforeMount');
  },

  mounted() {
    console.log('App.vue mounted');
  }
}
</script>

最重要的修改在entry-server.js中,

entry-server.js

import { createApp } from './app.js';

export default context => {
  return new Promise((resolve, reject) => {
    const { app, store, router, App } = createApp();

    router.push(context.url);

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();

      console.log(context.url)
      console.log(matchedComponents)

      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }

      Promise.all(matchedComponents.map(component => {
        if (component.asyncData) {
          return component.asyncData({ store });
        }
      })).then(() => {
        // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中
        context.state = store.state;

        // 返回根组件
        resolve(app);
      });
    }, reject);
  });
}

这里前面提到的context就起了大作用,它将用户访问的url地址传进来,供vue-router使用。因为有异步组件,所以在router.onReady的成功回调中,去找该url路由所匹配到的组件,获取异步数据那一套还和前面的一样。

于是,我们就完成了一个基本完整的基于Vue + VueRouter + VuexSSR配置,完成代码查看github

最终效果演示:

访问http://localhost:3000/bar

ssr-router

完整代码查看github

后续

上面我们通过五个步骤,完成了从纯浏览器渲染到完整服务端渲染的同构,代码既可以运行在浏览器端,也可以运行在服务器端。那么,回过头来我们在看一下是否有优化的空间,又或者有哪些扩展的思考。

1. 优化
  • 我们目前是使用renderToString方法,完全生成html后,才会向客户端返回,如果使用renderToStream,应用bigpipe技术可以向浏览器持续不断的返回一个流,那么文件的加载浏览器可以尽早的显示一些东西出来。
const stream = renderer.renderToStream(context)

返回的值是 Node.js stream

let html = ''

stream.on('data', data => {
  html += data.toString()
})

stream.on('end', () => {
  console.log(html) // 渲染完成
})

stream.on('error', err => {
  // handle error...
})

在流式渲染模式下,当 renderer 遍历虚拟 DOM 树(virtual DOM tree)时,会尽快发送数据。这意味着我们可以尽快获得"第一个 chunk",并开始更快地将其发送给客户端。

然而,当第一个数据 chunk 被发出时,子组件甚至可能不被实例化,它们的生命周期钩子也不会被调用。这意味着,如果子组件需要在其生命周期钩子函数中,将数据附加到渲染上下文(render context),当流(stream)启动时,这些数据将不可用。这是因为,大量上下文信息(context information)(如头信息(head information)或内联关键 CSS(inline critical CSS))需要在应用程序标记(markup)之前出现,我们基本上必须等待流(stream)完成后,才能开始使用这些上下文数据。

因此,如果你依赖由组件生命周期钩子函数填充的上下文数据,则不建议使用流式传输模式。

  • webpack优化

webpack优化又是一个大的话题了,这里不展开讨论,感兴趣的同学可以自行查找一些资料,后续我也可能会专门写一篇文章来讲webpack优化。

2. 思考
  • 是否必须使用vuex

答案是不用。Vuex只是为了帮助你实现一套数据存储、更新、获取的机制,入股你不用Vuex,那么你就必须自己想一套方案可以将异步获取到的数据存起来,并且在适当的时机将它注入到组件内,有一些文章提出了一些方案,我会放到参考文章里,大家可以阅读一下。

  • 是否使用SSR就一定好?

这个也是不一定的,任何技术都有使用场景。SSR可以帮助你提升首页加载速度,优化搜索引擎SEO,但同时由于它需要在node中渲染整套Vue的模板,会占用服务器负载,同时只会执行beforeCreatecreated两个生命周期,对于一些外部扩展库需要做一定处理才可以在SSR中运行等等。

结语

本文通过五个步骤,从纯浏览器端渲染开始,到配置一个完整的基于Vue + vue-router + Vuex的SSR环境,介绍了很多新的概念,也许你看完一遍不太理解,那么结合着源码,去自己手敲几遍,然后再来看几遍文章,相信你一定可以掌握SSR

最后,本文所有源代码都放在我的github上,如果对你有帮助的话,就来点一个赞吧~~

欢迎关注我的公众号

微信公众号

参考链接

查看原文

码农小志 收藏了文章 · 2019-07-22

vue-cli3 实现多页面应用

适用场景

公司有专门的审批系统,我现在做的后台管理系统需要接入,移动端和PC端都要有一个页面来展示业务信息。没必要开俩项目,所以打算用多页面来解决,PC和移动端放到一个项目里,然后打包到各自的文件夹。

简单来说,多页面也就是适用于有多个小页面,不至于单独开多个项目的情况。

项目结构

项目 src 文件夹结构如下:

打包之后 dist 文件夹结构如下:

修改哪些文件

  • 新增 utils 文件夹

utils 文件夹下新增四个文件:

getPages.js      // 用来获取 pages 文件夹下的文件名称,vue.config.js 使用
cssCopy.js       // webpack 打包之后各页面的 css 文件复制到各个文件夹下
jsCopy.js        // webpack 打包之后各页面的 js 文件复制到各个文件夹下
htmlReplace.js   // 解决打包之后各页面 html 文件引入的 css、js 文件的路径问题

getPages 引入到 vue.config.js 使用,其他三个打包时 node 执行。

  "scripts": {
    "dev": "vue-cli-service serve",
    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build && node utils/jsCopy.js && node utils/cssCopy.js && node utils/htmlReplace.js",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit"
  },
  • vue.config.js

为何添加 pages字段可参考 Vue 官方文档

+ let  pageMethod  =  require('./utils/getPages.js')
+ let  pages  =  pageMethod.pages()

module.exports  = {
-    // publicPath: './',  // 注意此行,会影响 htmlReplace.js 文件
+    pages
}

注意事项

每个页面的.html文件必须不能同名,不然本地开发环境无法拆分页面,本地如果想访问其他页面的话,地址如下:http://localhost:8080/order.html

最好的处理方式是每个页面的文件夹名字和.vue名字和.html名字和.js的名字都一样,这样打包时容易拆分。

参考

其实这篇文章不算原创,是参考其他文章写的,因为原文章真的是太简陋了....

原文地址:CSDN - lizhen_software
示例仓库地址:vue-more-pages

查看原文

码农小志 收藏了文章 · 2019-07-20

axios添加自定义头部出现的问题

在浏览器的http的GET,POST的跨域请求中,添加自定义头部,发送不是字符串,fromData的数据时,都是非简单请求。浏览器都预先发出OPTIONS(预检安全请求)。所以我们在axios中添加自定义头部时候需要后端返回OPTIONS请求允许才进行POST或GET请求。

后端中只需要对OPTIONS请求做出允许自定义头部和状态200即可。

if(req.method==='OPTIONS'){
        res.writeHead(200,{
            //允许跨域
            "Access-Control-Allow-Origin":"*",
            "Access-Control-Allow-Credentials":"true",
            //允许请求类型
            "Access-Control-Allow-Methods":"*",
            //允许自定义头部
            'Access-Control-Allow-Headers':"*",
            'Access-Control-Expose-Headers':'*'
        })
        res.end();
    }
查看原文

码农小志 赞了回答 · 2019-07-17

解决mysql中取出的时间格式不正确

moment(news_data_auto).format("Y-MM-DD HH:mm:ss");

关注 3 回答 2

码农小志 收藏了文章 · 2019-07-16

浅谈async·await

前言

这篇文章主要是梳理一下自己对阮一峰大神写的关于async/await文章,有写得不对的地方以及理解得不对的地方,各位大佬请指错!

对比

简单对比传统异步promise异步async异步

下文都会以setTimeout来进行异步展示,方便理解。

传统的回调

setTimeout(callback,1000);

function callback(){
    console.log("拿到结果了!");
}

setTimeout函数传入了两个参数(1000/callback),setTimeout被调用的时候,主线程不会等待1秒,而是先执行别的任务。callback这个函数就是一个回调函数,即当1秒后,主线程会重新调用callback(这里也不再啰嗦去说event Loop方面的知识了);

那么,当我们异步函数需要嵌套的时候呢。比如这种情况:

setTimeout(function(){
    console.log("第一个异步回调了!")
    setTimeout(function(){
        console.log("第二个异步回调了!")
        setTimeout(function(){
            console.log("第三个异步回调了!")
            setTimeout(function(){
                console.log("第四个异步回调了!")
                setTimeout(function(){
                    console.log("第五个异步回调了!")
                },1000);
            },1000);
        },1000);
    },1000);
},1000);

OK,想死不?

我们用promise来处理

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

timeout(2000)
  .then(value => {
    console.log("第一层" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第二层" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第三层" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第四层" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第五层" + value);
    return timeout(2000);
  })
  .catch(err => {
    console.log(err);
  });

OK,好看点了!

但是还是不方便!

我们用async/await来处理:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}
async function asyncTimeSys(){
    await timeout(1000);
    console.log("第一层异步结束!")
    await timeout(1000);
    console.log("第二层异步结束!")
    await timeout(1000);
    console.log("第三层异步结束!")
    await timeout(1000);
    console.log("第四层异步结束!")
    await timeout(1000);
    console.log("第五层异步结束!")
    return "all finish";
}
asyncTimeSys().then((value)=>{
    console.log(value);
});

OK,舒服了!

在这个asyncTimeSys函数里面,所有的异步操作,写的跟同步函数没有什么两样!

async的原型

async函数到底是什么?其实他就是Genarator函数(生成器函数)的语法糖而已!

  • 内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样。完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

  • 更好的语义。

async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  • 更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

  • 返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

其实,async函数就是一个由Generator封装的异步环境,其内部是通过交换函数执行权,以及thunk函数来实现的!

用Generator函数封装异步请求

OK,我们简单的封装一个:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

function *times(){
    let result =yield timeout(1000);
    return "second next"
}

let gen = times();    //拿到生成器函数,gen可以理解为指针
let firstYield = gen.next(); //firstYield此时为gen指针指向的第一个yield右边的表达式,此时timeout(1000)被执行
console.log(firstYield);    //   firstYield = {value:Pomise,done:false};

//接下来就是将firstYield中的value里的promise拿出来,作为正常的Promise调用,如下:
firstYield.value.then(()=>{
    //当timeout异步结束之后,执行以下代码,再将gen指针执行下一个yield,由于以下没有yield了,所以gen.next()的value为return里的东西
    console.log("timeout finish");
    console.log(gen.next());    //{value: "second next", done: true}
}).catch((err)=>{

});

这样封装有什么用呢,yield返回回来的东西,还是得像promise那样调用。

我们先来看看同步的代码,先让它长得像async和await那样子:

function* times() {
  yield console.log(1);
  yield console.log(2);
  yield console.log(3);
  return "second next";
}

let gen = times();

let result = gen.next();

while (!result.done) {
    result = gen.next();
}

OK,非常像了,但是,这是同步的。异步请求必须得等到第一个yield执行完成之后,才能去执行第二个yield。我们如果改成异步,肯定会造成无限循环。

那么,times生成器里面如果都是异步的话,我们应该怎么调用呢?

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

function *times(){
    yield timeout(2000);
    yield timeout(2000);
    yield timeout(2000);
    return "finish all!";
}

let gen = times();

let gen1 = gen.next();
gen1.value.then(function(data){
    console.log(data+" one");

    let gen2 = gen.next();
    gen2.value.then(function(data){
        console.log(data+" two");

        let gen3 = gen.next();
        gen3.value.then(function(data){
            console.log(data+" three");



        })

    })

});

仔细观察可以发现,其实每一个value的.then()方法都会传入一个相同的回调函数,这意味着我们可以使用递归来流程化管理整个异步流程;

改造一下这个上边的代码;

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

function* times() {
  yield timeout(2000);
  yield timeout(2000);
  yield timeout(2000);
  return "finish all!";
}


function run(fn){
    let gen = fn();

    function next(){
        console.log("finish");
        let result = gen.next();
        if(result.done) return;
        result.value.then(next);
    }
    next();
}

run(times);

OK,现在我们可以使用run函数,使得生成器函数times里的异步请求,一步接着一步往下执行。

那么,这个run函数里边的next到底是什么呢,它其实是一个thunk函数

thunk函数

Thunk函数的诞生是源于一个编译器设计的问题:求值策略,即函数的参数到底应该何时求值。

看下边的代码,请思考什么时候进行求值:

var x = 1;
function f(m) {
    return m * 2;
}
f(x + 5);

试问:x+5这个表达式应该什么时候求值

  • 传值调用(call by value),即在进入函数体之间,先计算x+5的值,再将这个值(6)传入函数f,例如c语言,这种做法的好处是实现比较简单,但是有可能会造成性能损失。
  • 传名调用(call by name),即直接将表达式(x+5)传入函数体,只在用到它的时候求值。

OK,thunk函数究竟是什么:

编译器的传名调用实现,往往就是将参数放到一个临时函数之中,再将这个临时函数转入函数体,这个临时函数就叫做Thunk函数。

将上边的代码进行改造:

var thunk = function () {
    return x + 5;
};

function f(thunk) {
    return thunk() * 2;
}

js中的传名调用是什么呢,与真正的thunk有什么区别呢?

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

网上对于thunk的演示都是使用的fs模块的readFile方法来进行演示

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

其实,任何函数,只要参数有回调函数,就能写成Thunk函数的形式。下面是一个简单的Thunk函数转换器。

让我们用setTimeout来进行一次演示:

//正常版本的setTimeout;
setTimeout(function(data){
    console.log(data);
},1000,"finish");

//thunk版本的setTimeout
let thunk = function(time){
    return function(callback){
        return setTimeout(callback,time,"finish");
    }
}
let setTimeoutChunk = thunk(1000);
setTimeoutChunk(function(data){
    console.log(data);
});

现在回头看一看用Generator函数封装异步请求这一节中最后一个实例中,我们封装的timeout函数,他其实就是一个thunk函数,我在那一节中没有给大家说明这一条:

  • yield命令后面的必须是 Thunk 函数。

为什么Generator里面必须使用thunk函数呢,因为我们需要确保传入的值只有一个,利用其回调函数,来进行递归自动控制Generator函数的流程,接收和交还程序的执行权;

查看原文

码农小志 赞了回答 · 2019-07-08

解决mysql中取出的时间格式不正确

  • 既然是js ,就推荐moment将时间统一处理成时间戳,同时它也支持时区的转换,然后将时间戳 供各个端 去做格式化的显示。
  • 不同机器上,我猜测应该和本地的时间格式设置有关系。

关注 3 回答 2

认证与成就

  • 获得 1 次点赞
  • 获得 14 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-10
个人主页被 396 人浏览