Web前端之路无梦的冒险谭

VuePress源码阅读(四) -- 「成"站"之日」markd

2021-01-19  本文已影响0人  Nodreame
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 也可以~)
旅程正在继续 ✿✿ヽ(°▽°)ノ✿

上一篇下一篇

猜你喜欢

热点阅读