前言

接上文:【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码

最近工作比较忙,加之上个月生了小孩,小情人是各种折腾他爸妈,我们可以使用的独立时间片不多,虽然这块研究进展缓慢,但是一直做下去,肯定还是会有一些收获的

之前我们这个课题研究一直是做独立的研究,没有去看已有的解决方案,这个是为了保证一个自己独立的思维,无论独立的思维还是人格都是很重要的东西,然独学而无友则孤陋而寡闻,稍微有点自己的东西后,还是应该看看外面已有的东西了,今天的目标是mpvue,我们来看看其官方描述:

mpvue (github 地址请参见)是一个使用 Vue.js 开发小程序的前端框架。框架基于 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序环境中,从而为小程序开发引入了整套 Vue.js 开发体验。

使用 mpvue 开发小程序,你将在小程序技术体系的基础上获取到这样一些能力:

  • 彻底的组件化开发能力:提高代码复用性
  • 完整的 Vue.js 开发体验
  • 方便的 Vuex 数据管理方案:方便构建复杂应用
  • 快捷的 webpack 构建机制:自定义构建策略、开发阶段 hotReload
  • 支持使用 npm 外部依赖
  • 使用 Vue.js 命令行工具 vue-cli 快速初始化项目
  • H5 代码转换编译成小程序目标代码的能力

其它特性正在等着你去探索,详细文档,github地址:https://github.com/Meituan-Dianping/mpvue

似乎,mpvue已经完成了我们想要做的工作,如果他真的好用,我们去学习吸收他也不失为一个好的方式,于是我们便去试试他的5分钟上手教程,简单编译后便形成了小程序代码,运行之:

这里最终生成的代码已经可以完全适配小程序了,我们这里主要来看看其app.json的配置:

{
"pages": [
"pages/index/main",
"pages/logs/main",
"pages/counter/main"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle": "black"
}
}

这里设置了起始页面,每个目录下都是main作为入口,我们简单看一下main的写法

<import src="/pages/index/index.vue.wxml" /><template is="b26bd43a" data="{{ ...$root['0'], $root }}"/>

都很一致,其中奇怪的template id就是真实的模板,然后我们看看源文件src:

<template>
<div class="container" @click="clickHandle('test click', $event)"> <div class="userinfo" @click="bindViewTap">
<img class="userinfo-avatar" v-if="userInfo.avatarUrl" :src="userInfo.avatarUrl" background-size="cover" />
<div class="userinfo-nickname">
<card :text="userInfo.nickName"></card>
</div>
</div> <div class="usermotto">
<div class="user-motto">
<card :text="motto"></card>
</div>
</div> <form class="form-container">
<input type="text" class="form-control" v-model="motto" placeholder="v-model" />
<input type="text" class="form-control" v-model.lazy="motto" placeholder="v-model.lazy" />
</form>
<a href="/pages/counter/main" class="counter">去往Vuex示例页面</a>
</div>
</template> <script>
import card from '@/components/card' export default {
data () {
return {
motto: 'Hello World',
userInfo: {}
}
}, components: {
card
}, methods: {
bindViewTap () {
const url = '../logs/main'
wx.navigateTo({ url })
},
getUserInfo () {
// 调用登录接口
wx.login({
success: () => {
wx.getUserInfo({
success: (res) => {
this.userInfo = res.userInfo
}
})
}
})
},
clickHandle (msg, ev) {
console.log('clickHandle:', msg, ev)
}
}, created () {
// 调用应用实例的方法获取全局数据
this.getUserInfo()
}
}
</script> <style scoped>
.userinfo {
display: flex;
flex-direction: column;
align-items: center;
} .userinfo-avatar {
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
} .userinfo-nickname {
color: #aaa;
} .usermotto {
margin-top: 150px;
} .form-control {
display: block;
padding: 0 12px;
margin-bottom: 5px;
border: 1px solid #ccc;
} .counter {
display: inline-block;
margin: 10px auto;
padding: 5px 10px;
color: blue;
border: 1px solid blue;
}
</style>

index.vue

import Vue from 'vue'
import App from './index' const app = new Vue(App)
app.$mount()

mpvue原理研究

可以看到,mpvue经过一次编译后,通过一个制定的规则,将vue的写法的页面,变成了小程序可以识别的代码,这里我们再回看其实现部分描述:

mpvue 修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序环境中,从而为小程序开发引入了整套 Vue.js 开发体验
mpvue-template-compiler 提供了将 vue 的模板语法转换到小程序的 wxml 语法的能力

这里我们回到Vue的部分,稍加说明,Vue现在已经做为了一套完整的解决方案而存在,特别是weex的出现后,框架出了platforms目录,经过之前的学习,我们知道:

我们在项目中写的html结构会被翻译为虚拟dom vnode从而形成ast(虚拟语法树),只要有了这个ast,不管后续容器是什么,可以是浏览器、可以是服务器端、也可以是native端,我们可以轻易的根据这个ast生成我们要的html结构:

el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])

这段代码(数据结构)可以很轻易被翻译为html结构,比如:

1 <ul id='list'>
2 <li class='item'>Item 1</li>
3 <li class='item'>Item 2</li>
4 <li class='item'>Item 3</li>
5 </ul>

也可以很轻易的被映射成Native视图代码,而我们知道其实我们习惯中的html代码其实并不是必须的,比如这段代码事实上会被变成这个样子:

new Vue({
template: '<div a="aaa"><div></div></div>'
})

===>等价的

new Vue({
render: function () {
return this._h('div', {
attrs:{
a: 'aaa'
}
}, [
this._h('div')
])
}
})

platforms的工作就是解决的是要讲ast转换为html结构还是native页面,显然,我们拿着vue解析后的ast可以形成小程序能够识别的代码片段,为了证明我们的猜想我们来看看mpvue的代码(详情看这里):

在web环境下,我们直接借用snabbdom对比两颗虚拟DOM的差异,直接完成渲染生成HTML,而mpvue完成的工作是将Vue的html模板编译为小程序识别的代码,其中一些差异,比如vue模板中指令处理会全部被磨平,我们这里来看一段代码,以下是for指令编译时候的处理:

export default {
if: 'wx:if',
iterator1: 'wx:for-index',
key: 'wx:key',
alias: 'wx:for-item',
'v-for': 'wx:for'
}
import astMap from '../config/astMap'

export default function (ast) {
const { iterator1, for: forText, key, alias, attrsMap } = ast if (forText) {
attrsMap[astMap['v-for']] = `{{${forText}}}`
if (iterator1) {
attrsMap[astMap['iterator1']] = iterator1
}
if (key) {
attrsMap[astMap['key']] = key
}
if (alias) {
attrsMap[astMap['alias']] = alias
} delete attrsMap['v-for']
} return ast
}

可以看到,mpvue其实在vue的基础上,在vue标签的处理下,改变ast中的一些属性,对等翻译成了小程序识别的代码,当然截止此时都还只是一些猜想,我们接下来深入demo的核心看看

代码编译-webpack

对于前端工程师来说,webpack已经成为了一种必备技能了,他包含了本地开发、编译压缩、性能优化的所有工作,这个是工程化统一化思维集大成的结果(虽然绕不开但有点难用)

webpack是现在最常用的JavaScript程序的静态模块打包器(module bundler),他的特点就是以模块(module)为中心,我们只要给一个入口文件,他会根据这个入口文件找到所有的依赖文件,最后捆绑到一起,这里盗个图:

这里几个核心概念是:

① 入口 - 指示webpack应该以哪个模块(一般是个js文件),作为内部依赖图的开始

② 输出 - 告诉将打包后的文件输出到哪里,或者文件名是什么

③ loader - 这个非常关键,这个让webpack能够去处理那些非JavaScript文件,或者是自定义文件,转换为可用的文件,比如将jsx转换为js,将less转换为css

test就是正则标志,标识哪些文件会被处理;use表示用哪个loader

④ 插件(plugins)

插件被用于转换某些类型的模块,适用于的范围更广,包括打包优化、压缩、重新定义环境中的变量等等,这里举一个小例子进行说明,react中的jsx这种事实上是浏览器直接不能识别的,但是我们却可以利用webpack将之进行一次编译:

// 原 JSX 语法代码
return <h1>Hello,Webpack</h1> // 被转换成正常的 JavaScript 代码
return React.createElement('h1', null, 'Hello,Webpack')

这个便是Babel所做的工作,我们要做的就是为我们的项目提供这样的解析器,比如babel-preset-react

我们前面说过,mpvue是我们以vue的语法写代码,并将其编译为小程序识别的代码,而这个工作是由webpack执行的,所以我们来看看mpvue的几个配置文件:

{
"name": "my-project",
"version": "1.0.0",
"description": "A Mpvue project",
"author": "yexiaochai <549265480@qq.com>",
"private": true,
"scripts": {
"dev:wx": "node build/dev-server.js wx",
"start:wx": "npm run dev:wx",
"build:wx": "node build/build.js wx",
"dev:swan": "node build/dev-server.js swan",
"start:swan": "npm run dev:swan",
"build:swan": "node build/build.js swan",
"dev": "node build/dev-server.js wx",
"start": "npm run dev",
"build": "node build/build.js wx",
"lint": "eslint --ext .js,.vue src"
},
"dependencies": {
"mpvue": "^1.0.11",
"vuex": "^3.0.1"
},
"devDependencies": {
"mpvue-loader": "^1.1.2",
"mpvue-webpack-target": "^1.0.0",
"mpvue-template-compiler": "^1.0.11",
"portfinder": "^1.0.13",
"postcss-mpvue-wxss": "^1.0.0",
"prettier": "~1.12.1",
"px2rpx-loader": "^0.1.10",
"babel-core": "^6.22.1",
"glob": "^7.1.2",
"webpack-mpvue-asset-plugin": "^0.1.1",
"relative": "^3.0.2",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.4.0",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.11",
"cssnano": "^3.10.0",
"eslint": "^4.19.1",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-html": "^4.0.3",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.16.3",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"http-proxy-middleware": "^0.18.0",
"webpack-bundle-analyzer": "^2.2.1",
"semver": "^5.3.0",
"shelljs": "^0.8.1",
"uglifyjs-webpack-plugin": "^1.2.5",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^2.0.0",
"rimraf": "^2.6.0",
"url-loader": "^1.0.1",
"vue-style-loader": "^4.1.0",
"webpack": "^3.11.0",
"webpack-dev-middleware-hard-disk": "^1.12.0",
"webpack-merge": "^4.1.0",
"postcss-loader": "^2.1.4"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

package.json

"scripts": {
"dev:wx": "node build/dev-server.js wx",
"start:wx": "npm run dev:wx",
"build:wx": "node build/build.js wx",
"dev:swan": "node build/dev-server.js swan",
"start:swan": "npm run dev:swan",
"build:swan": "node build/build.js swan",
"dev": "node build/dev-server.js wx",
"start": "npm run dev",
"build": "node build/build.js wx",
"lint": "eslint --ext .js,.vue src"
},

然后我们看看其webpack的配置(build/dev-server.js):

require('./check-versions')()

process.env.PLATFORM = process.argv[process.argv.length - 1] || 'wx'
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
} // var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var portfinder = require('portfinder')
var webpackConfig = require('./webpack.dev.conf') // default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable console.log('========') console.log(webpackConfig) var app = express()
var compiler = webpack(webpackConfig) // var devMiddleware = require('webpack-dev-middleware')(compiler, {
// publicPath: webpackConfig.output.publicPath,
// quiet: true
// }) // var hotMiddleware = require('webpack-hot-middleware')(compiler, {
// log: false,
// heartbeat: 2000
// })
// force page reload when html-webpack-plugin template changes
// compiler.plugin('compilation', function (compilation) {
// compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
// hotMiddleware.publish({ action: 'reload' })
// cb()
// })
// }) // proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
}) // handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')()) // serve webpack bundle output
// app.use(devMiddleware) // enable hot-reload and state-preserving
// compilation error display
// app.use(hotMiddleware) // serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static')) // var uri = 'http://localhost:' + port var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
}) // console.log('> Starting dev server...')
// devMiddleware.waitUntilValid(() => {
// console.log('> Listening at ' + uri + '\n')
// // when env is testing, don't need open it
// if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
// opn(uri)
// }
// _resolve()
// }) module.exports = new Promise((resolve, reject) => {
portfinder.basePort = port
portfinder.getPortPromise()
.then(newPort => {
if (port !== newPort) {
console.log(`${port}端口被占用,开启新端口${newPort}`)
}
var server = app.listen(newPort, 'localhost')
// for 小程序的文件保存机制
require('webpack-dev-middleware-hard-disk')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
resolve({
ready: readyPromise,
close: () => {
server.close()
}
})
}).catch(error => {
console.log('没有找到空闲端口,请打开任务管理器杀死进程端口再试', error)
})
})

里面比较关键的代码在此:

 var path = require('path')
var fs = require('fs')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
var MpvuePlugin = require('webpack-mpvue-asset-plugin')
var glob = require('glob')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var relative = require('relative') function resolve (dir) {
return path.join(__dirname, '..', dir)
} function getEntry (rootSrc) {
var map = {};
glob.sync(rootSrc + '/pages/**/main.js')
.forEach(file => {
var key = relative(rootSrc, file).replace('.js', '');
map[key] = file;
})
return map;
} const appEntry = { app: resolve('./src/main.js') }
const pagesEntry = getEntry(resolve('./src'), 'pages/**/main.js')
const entry = Object.assign({}, appEntry, pagesEntry) module.exports = {
// 如果要自定义生成的 dist 目录里面的文件路径,
// 可以将 entry 写成 {'toPath': 'fromPath'} 的形式,
// toPath 为相对于 dist 的路径, 例:index/demo,则生成的文件地址为 dist/index/demo.js
entry,
target: require('mpvue-webpack-target'),
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue': 'mpvue',
'@': resolve('src')
},
symlinks: false,
aliasFields: ['mpvue', 'weapp', 'browser'],
mainFields: ['browser', 'module', 'main']
},
module: {
rules: [
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.vue$/,
loader: 'mpvue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
include: [resolve('src'), resolve('test')],
use: [
'babel-loader',
{
loader: 'mpvue-loader',
options: Object.assign({checkMPEntry: true}, vueLoaderConfig)
},
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[ext]')
}
}
]
},
plugins: [
new MpvuePlugin(),
new CopyWebpackPlugin([{
from: '**/*.json',
to: ''
}], {
context: 'src/'
}),
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: path.resolve(config.build.assetsRoot, './static'),
ignore: ['.*']
}
])
]
}

webpack.base.conf.js

{
test: /\.vue$/,
loader: 'mpvue-loader',
options: vueLoaderConfig
},

关键就落到了我们这里的mpvue-loader了,他是自 vue-loader 修改而来,主要为 webpack 打包 mpvue components 提供能力,mpvue-loader 是 vue-loader 的一个扩展延伸版,类似于超集的关系,除了 vue-loader 本身所具备的能力之外,它还会产出微信小程序所需要的文件结构和模块内容。

详细的说明文档在:http://mpvue.com/build/mpvue-loader/https://github.com/mpvue/mpvue-loader

我们这里简单看看他是怎么做的,这里以wxml做下研究,先看看这个简单的转换:

<template>
<div class="my-component">
<h1>{{msg}}</h1>
<other-component :msg="msg"></other-component>
</div>
</template>

模板部分会变成这个样子:

<import src="components/other-component$hash.wxml" />
<template name="component$hash">
<view class="my-component">
<view class="_h1">{{msg}}</view>
<template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template>
</view>
</template>

而这块工作的进行在这:mpvue-template-compiler 提供了将 vue 的模板语法转换到小程序的 wxml 语法的能力

这里的代码有点多,涉及到了很多东西,今天篇幅很大了,等我们明天研究下vue-loader再继续学习吧......

一套代码小程序&Web&Native运行的探索07——mpvue简单调研的更多相关文章

  1. 一套代码小程序&Web&Native运行的探索06——组件系统

    接上文:一套代码小程序&Web&Native运行的探索05——snabbdom 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tre ...

  2. 一套代码小程序&Web&Native运行的探索05——snabbdom

    接上文:一套代码小程序&Web&Native运行的探索04——数据更新 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/ma ...

  3. 一套代码小程序&Web&Native运行的探索04——数据更新

    接上文:一套代码小程序&Web&Native运行的探索03 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/m ...

  4. 一套代码小程序&Web&Native运行的探索03——处理模板及属性

    接上文:一套代码小程序&Web&Native运行的探索02 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/m ...

  5. 一套代码小程序&Web&Native运行的探索02

    接上文:一套代码小程序&Web&Native运行的探索01,本文都是一些探索性为目的的研究学习,在最终版输出前,内中的内容可能会有点乱 参考: https://github.com/f ...

  6. 一套代码小程序&Web&Native运行的探索01

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 并且用小程序翻写了之前一个demo:[组件化开发]前端进阶篇之如何编写可维护可升级的代码 之前一直在跟业务方打交道 ...

  7. 【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...

  8. 小程序 web 端实时运行工具

    微信小程序 web 端实时运行工具 https://chemzqm.github.io/wept/

  9. 小程序实践(十):textarea实现简单的编辑文本界面

    textarea是官方的原生组件,用于多行输入 简单的例子,监听文本内容.长度,以及设置最大可输入文本长度 wxml <view class='textarea-Style'> <t ...

随机推荐

  1. mutex.go

    package} } ) ].GetResponseRange().Kvs[].CreateRevision )     return nil } func (m *Mutex) IsOwner() ...

  2. 单机配置kafka和zookeeper

    1:环境准备 jdk 推荐oracle,不建议open sdk 在/etc/profile加入下列环境变量 在PATH中将jdk和jre的bin加入path里面 $JAVA_HOME/bin:$JRE ...

  3. 记一次logback传输日志到logstash根据自定义设置动态创建ElasticSearch索引

    先说背景,由于本人工作需要创建很多小应用程序,而且在微服务的大环境下,服务越来越多,然后就导致日志四分五裂,到处都有,然后就有的elk,那么问题来了 不能每个小应用都配置一个 logstash 服务来 ...

  4. Go中原始套接字的深度实践

    1. 介绍 2. 传输层socket 2.1 ICMP 2.2 TCP 2.3 传输层协议 3. 网络层socket 3.1 使用Go库 3.2 系统调用 3.3 网络层协议 4. 总结 4.1 参考 ...

  5. Java相关面试题总结

    本文分为十九个模块,分别是: Java 基础.容器.多线程.反射.对象拷贝.Java Web .异常.网络.设计模式.Spring/Spring MVC.Spring Boot/Spring Clou ...

  6. Python中的那些“坑”

    1.哪个是True,哪个是False? 这里要看三组代码: # 第一组: >>>a=256 >>>b = 256 >>>a is b # 第二组: ...

  7. 如何给列表降维?sum()函数的妙用

    上个月,学习群里的 S 同学问了个题目,大意可理解为列表降维 ,例子如下: oldlist = [[1, 2, 3], [4, 5]] # 想得到结果:newlist = [1, 2, 3, 4, 5 ...

  8. 面试前必须要知道的Redis面试题

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 回顾前面: 从零单排学Redis[青铜] 从零单排学 ...

  9. WPF自定义控件创建

    WPF自定义控件创建 本文简单的介绍一下WPF自定义控件的开发. 首先,我们打开VisualStudio创建一个WPF自定义控件库,如下图: 然后,我们可以看到创建的解决方案如下: 在解决方案中,我们 ...

  10. Java设置PDF有序、无序列表

    文档中的设置有序或无序列表是一种反应内容上下级关系或者内容相同属性的方式,与单纯的文字叙述相比,它能有效增强文档内容的条理性,突出重点.因此,本文将分享通过Java编程在PDF文档中设置有序或无序列表 ...