VuePress源码阅读(四) -- 「成"站"之日」markdown建站实战篇

image

系列文章:

既然在上篇文章完成了对 markdown建站的分析,那么肯定要来个实战了,话不多说马上开始.

一、用 markdown 文件实现最简建站

这次实战的目标是使用一个 README.md 文件通过 Webpack 构建网站,支持简单的预览和打包.

mkdir vuepress-study
cd vuepress-study
mkdir docs
echo '# Hello VuePress' > docs/README.md
touch webpack.config.js
# 安装依赖
yarn init -y
yarn add -D webpack webpack-cli markdown-it html-webpack-plugin clean-webpack-plugin html-loader

这里先通过 webpack.config.js 配置来编写,之后再用 webpack-chain 实现一遍.

使用 markdown-it 的 demo 作为 README.md 的内容方便测试 markdown-it 的效果,链接为:https://markdown-it.github.io/

由于现在只有一个 markdown 文件(docs/README.md),所以作为 Webpack 入口的 js 和 markdown-loader 都需要由我们自行提供:

mkdir pkg
mkdir pkg/client
touch pkg/client/clientEntry.js
mkdir pkg/markdown-loader
touch pkg/markdown-loader/index.js

将 Webpack 官网例子改成引入 README.md 就形成了入口文件 clientEntry.js ,目标是网站能够将 markdown 转换结果添加到页面上:

import mdHTML from '../../docs/README.md'

function component() {
  var element = document.createElement('div');
  console.log(typeof mdHTML)
  element.innerHTML = mdHTML
  return element;
}

document.body.appendChild(component());

接着简单地引入 markdown-it 编写出 markdown-loader:

'use strict'

const md = require('markdown-it')

module.exports = function (content) {
  const markdown = md()
  const html= markdown.render(content)
  return html
}

最后编写 webpack.config.js 如下:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: path.resolve(__dirname, 'pkg/client/clientEntry.js'),
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      { 
        test: /\.md$/,
        use: [
          {
            loader: 'html-loader',
          },
          {
            // 使用项目中自带的 loader
            loader: require.resolve('./pkg/loader/markdown-loader'),
          }
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin(),
  ],
}

执行打包命令 npx webpack 完成打包:

image-20210117182520391

用命令行查看打包结果,并使用 http-server 运行本地服务器:

image-20210117182731257

OK,除了一些 emoji 没显示图像,大部分布局基本一致.

image-20210117182918098

这部分代码已经放到 Github 上了有兴趣可以看看:

二、整合 Vue 构建网站

1. 使用 Webpack + Vue 构建网站

上面只是个纯静态的最简网站搭建,而 VuePress 是基于 Vue 构建的,那么现在我们也来模拟这个流程.

首先是跑起在当前项目加入 vue,然后创建Vue实例并完成相应打包.

所以修改入口文件 pkg/client/clientEntry.js 为:

import { createApp } from './app'

const { app } = createApp()

app.$mount('#app')

其中的 createApp 工厂函数就是用来创建新的 Vue 实例的,所以编写 pkg/client/app.js 如下:

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

// 导出一个工厂函数,用于创建新实例
export function createApp () {
  const app = new Vue({
    // 根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app }
}

创建 App.vue 如下:

<template>
    <div id="vueapp">{{ msg }}</div>
</template>
<script>
    export default {
        data() {
            return {
                msg: 'Hello world!'
            }
        }
    }
</script>

并且准备好 index.html 以供 html-webpack-plugin 使用:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title></title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

接着安装对应依赖,其中 vue-loader 和 vue-template-compiler 需要一起安装,保证处于同个版本:

yarn add vue
yarn add -D vue-loader vue-template-compiler

最后修改 webpack.config.js 如下:

image-20210117205553060

接着运行 npx webpack 完成打包,可以看到页面已经成功显示出 App.vue 的内容:

image-20210117205839278

这部分代码已经放到 Github 上了有兴趣可以看看:

2. 将 markdown 编译结果插入 Vue 网站

上面我们已经将 Vue 整合到项目中并成功地完成了构建,但是由于入口文件 pkg/client/clientEntry.js 的逻辑变化导致网站并没有引用 markdown 的打包结果.

因为现在还没有加入 vue-router 来做路由管理,所以暂时使用比较原始的方法做过渡 -- 直接在 App.vue 做引入(由于直接返回字符串所以用 v-html 实现展示):

image-20210117212321731

效果如下:

image-20210117212456633

OK,至此我们已经使用 markdown 文件成功地创建了一个Vue 驱动的 SPA 网站,并且实现客户端渲染(CSR)方式的打包.

三. 实现服务端渲染(SSR)方式打包

回忆一下前几篇文章里面 VuePress 打包都是构建为 SSR 形式的客户端激活程序+服务端渲染程序,再运行服务端渲染程序生成SSR HTML,这里我们也来实践一下.

首先完成依赖的安装:

yarn add vue vue-server-renderer
yarn add -D webpack-node-externals

构建 SSR 的客户端激活部分的入口文件依旧使用 client/clientEntry.js 即可,而服务端渲染程序则需要替换为 client/serverEntry.js:

import { createApp } from './app'

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

由于CSR 和 SSR 的配置并不相同,所以这里将公共配置提取到 webpack.base.config.js ,配置文件 webpack.ssrclient.config.js 和 webpack.ssrserver.config.js 合并公共配置,然后用来处理 SSR 打包,,以此来生成 client.json 和 server.json.

1. 提取公共配置文件

webpack.base.config.js 的配置如下,主要是提取了 loader 和 VueLoaderPlugin:

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

module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
      { 
        test: /\.md$/,
        use: [
          {
            loader: 'html-loader',
          },
          {
            // 使用项目中自带的 loader
            loader: require.resolve('./pkg/loader/markdown-loader'),
          }
        ],
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
  ]
}

2. SSR客户端程序打包

webpack.ssrclient.config.js 的配置如下,这是参考 Vue SSR -- 构建配置-客户端配置 写的,但是由于 webpack4 后期已经取消掉 webpack.optimize.CommonsChunkPlugin,所以这里我使用 splitChunks 来替代:

const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = merge(baseConfig, {
  mode: 'production',
  entry: './pkg/client/clientEntry.js',
  plugins: [
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: "manifest",
    //   minChunks: Infinity
    // }),
    // 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。
    new VueSSRClientPlugin()
  ],
  optimization: {
    splitChunks: {
      name: 'manifest',
      minChunks: Infinity
    }
  },
})

运行 npx webpack --config webpack.ssrclient.config.js 进行打包:

image-20210117225750010

结果生成了 main.js 和对应的 vue-ssr-client-manifest.json:

image-20210117225656303

3.SSR服务端程序打包

webpack.ssrserver.js 的配置如下:

const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(baseConfig, {
  mode: 'production',
  entry: './pkg/client/serverEntry.js',
  target: 'node',
  devtool: 'source-map',
  output: {
    libraryTarget: 'commonjs2',
  },
  externals: nodeExternals({
    allowlist: /\.css$/
  }),
  // 这是将服务器的整个输出
  // 构建为单个 JSON 文件的插件。
  // 默认文件名为 `vue-ssr-server-bundle.json`
  plugins: [
    new VueSSRServerPlugin()
  ]
})

第一次运行的结果:

image-20210117231427323

检查我的配置编写应该没有问题之后,想起 VuePress 用的 Webpack4,而我用的是 Webpack5 所以可能会出一些问题,所以网上搜索了一下找到了解决方法 https://github.com/vuejs/vue/issues/11718,为了方便大家查看我就把重点截一下图:

image-20210117233227867

刚好一个人问了这个问题然后上面这个老哥回复了解决方案,需要修改 node_modules/vue-server-renderer/server-plugin.js 的代码,当然这不是太工程化的处理方案,但现在暂时也只能这样解决了~(提出解决方案的老哥也提出 Webpack4 现在暂时是得到更广泛的支持的,如果想少遇点问题可以暂时切回 Webpack4).

修改之后的运行结果:

image-20210117233242604

现在打包结果目录的情况如下:

image-20210117233350998

4. 执行服务端渲染

上面我们已经成功获取到了两个json 文件,那么现在我们来编写服务端渲染程序,生成一个SSR HTML.

创建 pkg/client/index.ssr.html 如下:

<!DOCTYPE html>
<html lang="{{ lang }}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>{{ title }}</title>
    <meta name="generator" content="VuePress {{ version }}">
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

然后编写渲染程序 pkg/client/renderSSRHTML.js:

const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')

const serverBundle = require(path.resolve('../../dist/vue-ssr-server-bundle.json'))
const clientManifest = require(path.resolve('../../dist/vue-ssr-client-manifest.json'))

const ssrHTML = `<!DOCTYPE html>
<html lang="{{ lang }}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>{{ title }}</title>
    <meta name="generator" content="VuePress {{ version }}">
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>
`

async function main () {
  // create server renderer using built manifests
  const renderer = createBundleRenderer(serverBundle, {
    clientManifest,
    runInNewContext: false,
    inject: false,
    shouldPrefetch: () => true,
    template: ssrHTML
  })

  const context = {
    title: 'VuePress',
    lang: 'en',
    version: '1.0'
  }

  let html = await renderer.renderToString(context)
  fs.writeFile('../../dist/index.html', html, () => { console.log('finish') })
}

main()

执行服务端渲染程序:

image-20210118001958783

生成 index.html 成功:

image-20210118002026722

来看看效果,控制台中又看到了熟悉的 data-server-rendered 了~

image-20210118002155478

至此服务端渲染(SSR)方式打包渲染的方式已经基本实现,markdown经过层层处理终于成为一个网站了~

这部分代码已经放到 Github 上了有兴趣可以看看:

欢迎拍砖,觉得还行也欢迎点赞收藏~
新开公号:「无梦的冒险谭」欢迎关注(搜索 Nodreame 也可以~)
旅程正在继续 ✿✿ヽ(°▽°)ノ✿

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,809评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,189评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,290评论 0 359
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,399评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,425评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,116评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,710评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,629评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,155评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,261评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,399评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,068评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,758评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,252评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,381评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,747评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,402评论 2 358

推荐阅读更多精彩内容